diff --git a/config/routes.ts b/config/routes.ts index 27ff5087..fc82e6db 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -55,6 +55,10 @@ export default [ path: '/', redirect: '/welcome', }, + { + path: '/tts-mobile-pro', + component: './tts_mobile_pro', + }, { component: '404', layout: false, diff --git a/mock/requestRecord.mock.js b/mock/requestRecord.mock.js index 7c8f0de5..aba6c5d8 100644 --- a/mock/requestRecord.mock.js +++ b/mock/requestRecord.mock.js @@ -1,33 +1,5 @@ module.exports = { - 'GET /api/currentUser': { - data: { - name: 'Serati Ma', - avatar: - 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', - userid: '00000001', - email: 'antdesign@alipay.com', - signature: '海纳百川,有容乃大', - title: '交互专家', - group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED', - tags: [ - { key: '0', label: '很有想法的' }, - { key: '1', label: '专注设计' }, - { key: '2', label: '辣~' }, - { key: '3', label: '大长腿' }, - { key: '4', label: '川妹子' }, - { key: '5', label: '海纳百川' }, - ], - notifyCount: 12, - unreadCount: 11, - country: 'China', - geographic: { - province: { label: '浙江省', key: '330000' }, - city: { label: '杭州市', key: '330100' }, - }, - address: '西湖区工专路 77 号', - phone: '0752-268888888', - }, - }, + 'GET /api/rule': { data: [ { diff --git a/public/test.wav b/public/test.wav new file mode 100644 index 00000000..4d0b4efa Binary files /dev/null and b/public/test.wav differ diff --git a/src/locales/en-US/pages.ts b/src/locales/en-US/pages.ts index 34f2cb83..e826f0a1 100644 --- a/src/locales/en-US/pages.ts +++ b/src/locales/en-US/pages.ts @@ -73,4 +73,9 @@ export default { 'pages.searchTable.tenThousand': '0000', 'pages.searchTable.batchDeletion': 'batch deletion', 'pages.searchTable.batchApproval': 'batch approval', + 'tts.title': 'Text to Speech', + 'tts.register': 'Register', + 'tts.banner': 'Banner Image', + 'tts.inputPlaceholder': 'Enter Chinese/English', + 'tts.generate': 'Generate', }; diff --git a/src/locales/zh-CN/pages.ts b/src/locales/zh-CN/pages.ts index 22ac97a0..e4dae27c 100644 --- a/src/locales/zh-CN/pages.ts +++ b/src/locales/zh-CN/pages.ts @@ -68,4 +68,9 @@ export default { 'pages.searchTable.tenThousand': '万', 'pages.searchTable.batchDeletion': '批量删除', 'pages.searchTable.batchApproval': '批量审批', + 'tts.title': '文字转语音', + 'tts.register': '注册', + 'tts.banner': 'banner 图片', + 'tts.inputPlaceholder': '输入中英文', + 'tts.generate': '生成', }; diff --git a/src/pages/localsrc/test.wav b/src/pages/localsrc/test.wav new file mode 100644 index 00000000..4d0b4efa Binary files /dev/null and b/src/pages/localsrc/test.wav differ diff --git a/src/pages/tts_mobile_pro.less b/src/pages/tts_mobile_pro.less new file mode 100644 index 00000000..65460143 --- /dev/null +++ b/src/pages/tts_mobile_pro.less @@ -0,0 +1,358 @@ +/* AI科技多巴胺风格配色 */ +@primary-color: #00ff88; +@secondary-color: #00d4ff; +@accent-color: #ff6b6b; +@bg-color: #0a0e27; +@text-color: #ffffff; +@text-light: #b0b8d4; +@border-color: #1e2746; +@card-color: #121a33; +@shadow-color: rgba(0, 255, 136, 0.3); + +.container { + min-height: 100vh; + background: @bg-color; + color: @text-color; + overflow-x: hidden; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%); + border-bottom: 1px solid @border-color; + box-shadow: 0 2px 8px @shadow-color; + + .title { + font-size: 20px; + font-weight: bold; + color: @primary-color; + } + + .registerBtn { + border-color: @primary-color; + color: @primary-color; + &:hover { + background: @primary-color; + color: @bg-color; + } + } +} + +.banner { + padding: 24px; + background: linear-gradient(135deg, @secondary-color 0%, @primary-color 100%); + margin: 16px; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 212, 255, 0.4); + + .bannerPlaceholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + background: rgba(255, 255, 255, 0.1); + border-radius: 8px; + backdrop-filter: blur(10px); + + .bannerIcon { + font-size: 48px; + color: #ffffff; + margin-bottom: 12px; + } + + span { + font-size: 16px; + color: #ffffff; + font-weight: 500; + } + } +} + +.contentList { + padding: 0 16px 100px; + max-height: calc(100vh - 200px); + overflow-y: auto; + + // 自定义滚动条 + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: @border-color; + } + + &::-webkit-scrollbar-thumb { + background: @primary-color; + border-radius: 2px; + } +} + +.audioItem { + display: flex; + align-items: flex-start; + padding: 16px; + margin-bottom: 12px; + background: @card-color; + border-radius: 12px; + border: 1px solid @border-color; + transition: all 0.3s ease; + + &:hover { + border-color: @primary-color; + box-shadow: 0 4px 16px @shadow-color; + transform: translateY(-2px); + } +} + +.playBtnContainer { + position: relative; + margin-right: 16px; + flex-shrink: 0; +} + +.playBtn { + width: 56px; + height: 56px; + border-radius: 50%; + background: @border-color; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 48px; + height: 48px; + border-radius: 50%; + background: @card-color; + transform: translate(-50%, -50%); + } + + &:hover { + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0, 255, 136, 0.4); + } + + &.playing { + animation: pulse 1.5s ease-in-out infinite; + } +} + +@keyframes pulse { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.4); + } + 70% { + box-shadow: 0 0 0 10px rgba(0, 255, 136, 0); + } +} + +.playIcon { + font-size: 24px; + color: @primary-color; + position: relative; + z-index: 1; +} + +.textContent { + flex: 1; + font-size: 14px; + line-height: 1.6; + color: @text-light; + word-break: break-all; + margin-right: 16px; +} + +.menuBtn { + font-size: 20px; + color: @text-light; + cursor: pointer; + transition: color 0.3s ease; + flex-shrink: 0; + + &:hover { + color: @primary-color; + } +} + +.footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 16px; + background: linear-gradient(180deg, rgba(10, 14, 39, 0) 0%, @bg-color 50%); + + .inputContainer { + margin-bottom: 12px; + + :global(.ant-input) { + background: @card-color; + border: 1px solid @border-color; + color: @text-color; + border-radius: 8px; + transition: all 0.3s ease; + + &:focus { + border-color: @primary-color; + box-shadow: 0 0 0 2px @shadow-color; + } + + &::placeholder { + color: @text-light; + } + } + + :global(.ant-input-auto-size) { + min-height: 44px; + } + } + + .footerButtons { + display: flex; + justify-content: flex-end; + gap: 12px; + + .generateBtn { + background: linear-gradient(135deg, @primary-color 0%, @secondary-color 100%); + border: none; + height: 44px; + padding: 0 24px; + border-radius: 8px; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0, 255, 136, 0.4); + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 255, 136, 0.5); + } + + &:active { + transform: translateY(0); + } + } + + .settingBtn { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-color: @border-color; + color: @text-light; + transition: all 0.3s ease; + + &:hover { + border-color: @primary-color; + color: @primary-color; + box-shadow: 0 4px 12px rgba(0, 255, 136, 0.3); + } + } + } +} + +// 适配移动端 +@media (max-width: 480px) { + .container { + padding-bottom: 120px; + } + + .header { + padding: 12px 16px; + + .title { + font-size: 18px; + } + + .registerBtn { + font-size: 12px; + padding: 4px 12px; + } + } + + .banner { + padding: 16px; + margin: 12px; + + .bannerPlaceholder { + padding: 32px 16px; + + .bannerIcon { + font-size: 40px; + } + + span { + font-size: 14px; + } + } + } + + .contentList { + padding: 0 12px 100px; + } + + .audioItem { + padding: 12px; + margin-bottom: 10px; + } + + .playBtn { + width: 52px; + height: 52px; + + &::before { + width: 44px; + height: 44px; + } + + .playIcon { + font-size: 22px; + } + } + + .textContent { + font-size: 13px; + } + + .footer { + padding: 12px; + + .footerButtons { + .generateBtn { + padding: 0 20px; + font-size: 14px; + } + + .settingBtn { + width: 42px; + height: 42px; + } + } + } +} + +// 加载动画 +:global(.ant-pull-to-refresh-mask) { + color: @primary-color; +} + +:global(.ant-pull-to-refresh-indicator) { + color: @primary-color; +} + +:global(.ant-spin-dot-item) { + background-color: @primary-color; +} \ No newline at end of file diff --git a/src/pages/tts_mobile_pro.tsx b/src/pages/tts_mobile_pro.tsx new file mode 100644 index 00000000..5b2b5176 --- /dev/null +++ b/src/pages/tts_mobile_pro.tsx @@ -0,0 +1,229 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button, Input, List, Space, Spin, message, InfiniteScroll } from 'antd'; +import { PlayCircleOutlined, MenuOutlined, SettingOutlined, SoundOutlined } from '@ant-design/icons'; +import { useIntl } from 'umi'; +import styles from './tts_mobile_pro.less'; + +// 定义音频播放状态类型 +interface AudioItem { + id: string; + text: string; + audioUrl: string; + isPlaying: boolean; + progress: number; +} + +const TTSMobilePro: React.FC = () => { + const intl = useIntl(); + const [inputText, setInputText] = useState(''); + const [audioList, setAudioList] = useState([]); + const [loading, setLoading] = useState(false); + const [loadMoreLoading, setLoadMoreLoading] = useState(false); + const audioRefs = useRef>(new Map()); + const animationRefs = useRef>(new Map()); + + // 初始化测试数据 + useEffect(() => { + const testAudio: AudioItem = { + id: `test-${Date.now()}`, + text: '这是一条测试音频,用于演示文字转语音功能。', + audioUrl: '/test.wav', + isPlaying: false, + progress: 0, + }; + setAudioList([testAudio]); + }, []); + + // 播放音频 + const playAudio = (item: AudioItem) => { + const audio = audioRefs.current.get(item.id); + if (!audio) return; + + if (item.isPlaying) { + // 暂停 + audio.pause(); + cancelAnimationFrame(animationRefs.current.get(item.id) || 0); + } else { + // 播放 + audio.play().catch(err => { + message.error('音频播放失败:' + err.message); + }); + // 启动进度动画 + const updateProgress = () => { + if (audio.duration) { + const progress = (audio.currentTime / audio.duration) * 100; + setAudioList(prev => prev.map(a => + a.id === item.id ? { ...a, progress } : a + )); + } + animationRefs.current.set(item.id, requestAnimationFrame(updateProgress)); + }; + updateProgress(); + } + + // 更新播放状态 + setAudioList(prev => prev.map(a => + a.id === item.id ? { ...a, isPlaying: !a.isPlaying } : a + )); + }; + + // 音频结束事件 + const handleAudioEnded = (id: string) => { + setAudioList(prev => prev.map(a => + a.id === id ? { ...a, isPlaying: false, progress: 0 } : a + )); + cancelAnimationFrame(animationRefs.current.get(id) || 0); + }; + + // 生成语音 + const generateAudio = async () => { + if (!inputText.trim()) { + message.warning('请输入文本内容'); + return; + } + + setLoading(true); + try { + // 调用HTTP接口获取音频链接(这里模拟接口调用) + // 实际项目中替换为真实接口请求 + const mockAudioUrl = '/test.wav'; // 模拟返回的音频链接 + + // 创建新的音频条目 + const newItem: AudioItem = { + id: `audio-${Date.now()}`, + text: inputText.trim().substring(0, 20), // 只显示前20个字 + audioUrl: mockAudioUrl, + isPlaying: false, + progress: 0, + }; + + // 插入列表数据结构尾部(注意:最新的数据在最上面,所以需要unshift) + setAudioList(prev => [newItem, ...prev].slice(0, 10)); // 最多展示10条 + setInputText(''); // 清空输入框 + message.success('语音生成成功'); + } catch (error) { + message.error('语音生成失败:' + (error as Error).message); + } finally { + setLoading(false); + } + }; + + // 上拉加载更多 + const onLoadMore = () => { + setLoadMoreLoading(true); + // 模拟加载历史数据 + setTimeout(() => { + // 这里可以添加加载历史数据的逻辑 + setLoadMoreLoading(false); + message.info('已加载全部数据'); + }, 1500); + }; + + // 加载更多的组件 + const loadMore = () => { + return ( +
+ {loadMoreLoading ? : '上拉加载更多'} +
+ ); + }; + + // 渲染音频列表项 + const renderAudioItem = (item: AudioItem) => ( +
+ {/* 播放按钮 */} +
+
playAudio(item)} + > +
+ +
+
+ {/* 隐藏的音频元素 */} +
+ + {/* 文本内容 */} +
{item.text}
+ + {/* 菜单按钮 */} +
+ +
+
+ ); + + return ( +
+ {/* 顶部标题栏 */} +
+
{intl.formatMessage({ id: 'tts.title', defaultMessage: '文字转语音' })}
+ +
+ + {/* Banner区 */} +
+
+ + {intl.formatMessage({ id: 'tts.banner', defaultMessage: 'banner 图片' })} +
+
+ + {/* 内容列表 */} +
+ + + +
+ + {/* 底部输入区 */} +
+
+ setInputText(e.target.value)} + onPressEnter={generateAudio} + autoSize={{ minRows: 1, maxRows: 3 }} + /> +
+ + + +
+
+ ); +}; + +export default TTSMobilePro; \ No newline at end of file