Commit a01fd7ab020d82d02bf84582b1365e5a227e1467
1 parent
98ac06d9
refactor: use new ui
Showing
20 changed files
with
1154 additions
and
623 deletions
app.js
| ... | ... | @@ -70,29 +70,6 @@ App({ |
| 70 | 70 | } |
| 71 | 71 | ]; |
| 72 | 72 | }, |
| 73 | - onLaunch(option){ | |
| 74 | - const { query } = option; | |
| 75 | - // 获取渠道值 | |
| 76 | - this.globalData.cid = query.cid || ''; | |
| 77 | - // 挂载 request | |
| 78 | - const { domain, appid, cid } = this.globalData; | |
| 79 | - this.request = createRequest({ domain, appid, cid }); | |
| 80 | - | |
| 81 | - // 获取系统信息,计算安全区域 | |
| 82 | - const windowInfo = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); | |
| 83 | - const { statusBarHeight, safeArea, windowHeight, screenHeight } = windowInfo; | |
| 84 | - this.globalData.statusBarHeight = statusBarHeight; | |
| 85 | - this.globalData.safeAreaTop = safeArea.top; | |
| 86 | - this.globalData.safeAreaBottom = screenHeight - safeArea.bottom; | |
| 87 | - // 右上角胶囊位置信息 | |
| 88 | - const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); | |
| 89 | - // 导航栏高度,是胶囊以及胶囊的上下边距所包含的高度,通常是 32+4+4 | |
| 90 | - this.globalData.navBarHeight = menuButtonInfo.height + (menuButtonInfo.top - statusBarHeight) * 2; | |
| 91 | - this.globalData.totalNavHeight = statusBarHeight + this.globalData.navBarHeight; | |
| 92 | - this.globalData.contentHeight = windowHeight - this.globalData.totalNavHeight; | |
| 93 | - this.globalData.totalTabBarHeight = this.globalData.tabBarHeight + this.globalData.safeAreaBottom; | |
| 94 | - this.globalData.contentWithoutTabbarHeight = this.globalData.contentHeight - this.globalData.totalTabBarHeight; | |
| 95 | - }, | |
| 96 | 73 | // TTS接口请求方法 |
| 97 | 74 | async requestTTS(text, voice) { |
| 98 | 75 | return new Promise((resolve, reject) => { |
| ... | ... | @@ -165,5 +142,34 @@ App({ |
| 165 | 142 | } |
| 166 | 143 | return list; |
| 167 | 144 | } |
| 145 | + }, | |
| 146 | + onLaunch(option){ | |
| 147 | + const { query } = option; | |
| 148 | + // 获取渠道值 | |
| 149 | + this.globalData.cid = query.cid || ''; | |
| 150 | + // 挂载 request | |
| 151 | + const { domain, appid, cid } = this.globalData; | |
| 152 | + this.request = createRequest({ domain, appid, cid }); | |
| 153 | + | |
| 154 | + // 获取系统信息,计算安全区域 | |
| 155 | + const windowInfo = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); | |
| 156 | + const { statusBarHeight, safeArea, windowHeight, screenHeight } = windowInfo; | |
| 157 | + this.globalData.statusBarHeight = statusBarHeight; | |
| 158 | + this.globalData.safeAreaTop = safeArea.top; | |
| 159 | + this.globalData.safeAreaBottom = screenHeight - safeArea.bottom; | |
| 160 | + // 右上角胶囊位置信息 | |
| 161 | + const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); | |
| 162 | + // 导航栏高度,是胶囊以及胶囊的上下边距所包含的高度,通常是 32+4+4 | |
| 163 | + this.globalData.navBarHeight = menuButtonInfo.height + (menuButtonInfo.top - statusBarHeight) * 2; | |
| 164 | + this.globalData.totalNavHeight = statusBarHeight + this.globalData.navBarHeight; | |
| 165 | + this.globalData.contentHeight = windowHeight - this.globalData.totalNavHeight; | |
| 166 | + this.globalData.totalTabBarHeight = this.globalData.tabBarHeight + this.globalData.safeAreaBottom; | |
| 167 | + this.globalData.contentWithoutTabbarHeight = this.globalData.contentHeight - this.globalData.totalTabBarHeight; | |
| 168 | + | |
| 169 | + // 移除 2022-12-05 之前的本地存储,因为可能有敏感词存在 | |
| 170 | + const timestamp = new Date('2022-12-05').getTime(); | |
| 171 | + const audioList = this.globalData.getAudioList().filter(v => v.time > timestamp); | |
| 172 | + const storageKey = 'audioList'; | |
| 173 | + wx.setStorageSync(storageKey, audioList); | |
| 168 | 174 | } |
| 169 | 175 | }); | ... | ... |
app.json
| 1 | 1 | { |
| 2 | 2 | "pages": [ |
| 3 | 3 | "pages/index/index", |
| 4 | - "pages/result/result" | |
| 4 | + "pages/text-to-audio/text-to-audio", | |
| 5 | + "pages/text-to-audio-result/text-to-audio-result" | |
| 5 | 6 | ], |
| 6 | 7 | "window": { |
| 7 | 8 | "backgroundTextStyle": "light", |
| 8 | 9 | "navigationBarBackgroundColor": "#fff", |
| 9 | 10 | "navigationBarTitleText": "文字转语音", |
| 10 | - "navigationBarTextStyle": "black", | |
| 11 | + "navigationBarTextStyle": "white", | |
| 11 | 12 | "navigationStyle": "custom" |
| 12 | 13 | }, |
| 13 | 14 | "sitemapLocation": "sitemap.json" |
| 14 | -} | |
| 15 | 15 | \ No newline at end of file |
| 16 | +} | ... | ... |
app.wxss
| ... | ... | @@ -48,13 +48,9 @@ page { |
| 48 | 48 | padding-bottom: env(safe-area-inset-bottom); |
| 49 | 49 | } |
| 50 | 50 | |
| 51 | -/* 保留原有的 container 类以兼容其他页面 */ | |
| 52 | -.container { | |
| 53 | - width: 100%; | |
| 54 | - height: 100%; | |
| 55 | - display: flex; | |
| 56 | - flex-direction: column; | |
| 57 | - align-items: center; | |
| 58 | - justify-content: space-between; | |
| 59 | - box-sizing: border-box; | |
| 60 | -} | |
| 51 | +::-webkit-scrollbar { | |
| 52 | + display: none; | |
| 53 | + width: 0; | |
| 54 | + height: 0; | |
| 55 | + color: transparent; | |
| 56 | +} | |
| 61 | 57 | \ No newline at end of file | ... | ... |
components/custom-nav-bar/index.js
0 → 100644
| 1 | +Component({ | |
| 2 | + properties: { | |
| 3 | + // 导航栏标题 | |
| 4 | + title: { | |
| 5 | + type: String, | |
| 6 | + value: '' | |
| 7 | + }, | |
| 8 | + // 背景颜色 | |
| 9 | + background: { | |
| 10 | + type: String, | |
| 11 | + value: 'transparent' | |
| 12 | + }, | |
| 13 | + // 标题颜色 | |
| 14 | + color: { | |
| 15 | + type: String, | |
| 16 | + value: '#333333' | |
| 17 | + }, | |
| 18 | + // 是否显示返回按钮 | |
| 19 | + showBack: { | |
| 20 | + type: Boolean, | |
| 21 | + value: true | |
| 22 | + }, | |
| 23 | + // 返回按钮图标颜色 | |
| 24 | + backIconColor: { | |
| 25 | + type: String, | |
| 26 | + value: '#ffffff' | |
| 27 | + }, | |
| 28 | + // 是否显示首页按钮 | |
| 29 | + showHome: { | |
| 30 | + type: Boolean, | |
| 31 | + value: false | |
| 32 | + }, | |
| 33 | + // 特殊模式,不占高度 | |
| 34 | + noPlaceholder: { | |
| 35 | + type: Boolean, | |
| 36 | + value: false | |
| 37 | + } | |
| 38 | + }, | |
| 39 | + | |
| 40 | + data: { | |
| 41 | + statusBarHeight: 0, | |
| 42 | + navBarHeight: 44, | |
| 43 | + totalHeight: 0 | |
| 44 | + }, | |
| 45 | + | |
| 46 | + lifetimes: { | |
| 47 | + attached() { | |
| 48 | + const app = getApp() | |
| 49 | + | |
| 50 | + // 直接使用 app.globalData 中已经计算好的数据 | |
| 51 | + this.setData({ | |
| 52 | + statusBarHeight: app.globalData.statusBarHeight, | |
| 53 | + navBarHeight: app.globalData.navBarHeight, | |
| 54 | + totalHeight: app.globalData.totalNavHeight | |
| 55 | + }) | |
| 56 | + } | |
| 57 | + }, | |
| 58 | + | |
| 59 | + methods: { | |
| 60 | + // 返回上一页 | |
| 61 | + onBack() { | |
| 62 | + const pages = getCurrentPages() | |
| 63 | + if (pages.length > 1) { | |
| 64 | + wx.navigateBack({ | |
| 65 | + delta: 1 | |
| 66 | + }) | |
| 67 | + } else { | |
| 68 | + // 如果是第一个页面,尝试返回首页 | |
| 69 | + wx.switchTab({ | |
| 70 | + url: '/pages/index/index', | |
| 71 | + fail: () => { | |
| 72 | + wx.reLaunch({ | |
| 73 | + url: '/pages/index/index' | |
| 74 | + }) | |
| 75 | + } | |
| 76 | + }) | |
| 77 | + } | |
| 78 | + this.triggerEvent('back') | |
| 79 | + }, | |
| 80 | + | |
| 81 | + // 返回首页 | |
| 82 | + onHome() { | |
| 83 | + wx.switchTab({ | |
| 84 | + url: '/pages/index/index', | |
| 85 | + fail: () => { | |
| 86 | + wx.reLaunch({ | |
| 87 | + url: '/pages/index/index' | |
| 88 | + }) | |
| 89 | + } | |
| 90 | + }) | |
| 91 | + this.triggerEvent('home') | |
| 92 | + } | |
| 93 | + } | |
| 94 | +}) | ... | ... |
components/custom-nav-bar/index.json
0 → 100644
components/custom-nav-bar/index.wxml
0 → 100644
| 1 | +<view class="nav-bar" style="background: {{background}}; height: {{totalHeight}}px;"> | |
| 2 | + <!-- 状态栏占位 --> | |
| 3 | + <view class="status-bar" style="height: {{statusBarHeight}}px;"></view> | |
| 4 | + | |
| 5 | + <!-- 导航栏内容 --> | |
| 6 | + <view class="nav-bar-content" style="height: {{navBarHeight}}px;"> | |
| 7 | + <!-- 左侧返回按钮区域 --> | |
| 8 | + <view class="nav-bar-left"> | |
| 9 | + <view wx:if="{{showBack}}" class="nav-bar-btn" bindtap="onBack"> | |
| 10 | + <view class="back-icon" style="border-color: {{backIconColor}};"></view> | |
| 11 | + </view> | |
| 12 | + <view wx:if="{{showHome}}" class="nav-bar-btn" bindtap="onHome"> | |
| 13 | + <view class="home-icon" style="color: {{backIconColor}};">⌂</view> | |
| 14 | + </view> | |
| 15 | + </view> | |
| 16 | + | |
| 17 | + <!-- 中间标题 --> | |
| 18 | + <view class="nav-bar-title" style="color: {{color}};"> | |
| 19 | + {{title}} | |
| 20 | + </view> | |
| 21 | + | |
| 22 | + <!-- 右侧占位(保持标题居中) --> | |
| 23 | + <view class="nav-bar-right"> | |
| 24 | + <slot name="right"></slot> | |
| 25 | + </view> | |
| 26 | + </view> | |
| 27 | +</view> | |
| 28 | + | |
| 29 | +<!-- 占位元素,防止页面内容被导航栏遮挡 --> | |
| 30 | +<view class="nav-bar-placeholder" style="height: {{totalHeight}}px;"></view> | ... | ... |
components/custom-nav-bar/index.wxss
0 → 100644
| 1 | +.nav-bar { | |
| 2 | + position: fixed; | |
| 3 | + top: 0; | |
| 4 | + left: 0; | |
| 5 | + right: 0; | |
| 6 | + z-index: 9999; | |
| 7 | + background: transparent; | |
| 8 | +} | |
| 9 | + | |
| 10 | +.status-bar { | |
| 11 | + width: 100%; | |
| 12 | +} | |
| 13 | + | |
| 14 | +.nav-bar-content { | |
| 15 | + display: flex; | |
| 16 | + align-items: center; | |
| 17 | + justify-content: space-between; | |
| 18 | + position: relative; | |
| 19 | +} | |
| 20 | + | |
| 21 | +.nav-bar-left { | |
| 22 | + display: flex; | |
| 23 | + align-items: center; | |
| 24 | + flex-shrink: 0; | |
| 25 | + z-index: 2; | |
| 26 | +} | |
| 27 | + | |
| 28 | +.nav-bar-btn { | |
| 29 | + width: 32px; | |
| 30 | + height: 32px; | |
| 31 | + display: flex; | |
| 32 | + align-items: center; | |
| 33 | + justify-content: center; | |
| 34 | + margin-left: 8px; | |
| 35 | + margin-right: 4px; | |
| 36 | + border-radius: 50%; | |
| 37 | + background: rgba(255, 255, 255, 0.2); | |
| 38 | + backdrop-filter: blur(10px); | |
| 39 | + transition: all 0.3s ease; | |
| 40 | +} | |
| 41 | + | |
| 42 | +.nav-bar-btn:active { | |
| 43 | + background: rgba(255, 255, 255, 0.4); | |
| 44 | + transform: scale(0.95); | |
| 45 | +} | |
| 46 | + | |
| 47 | +/* 返回按钮箭头 */ | |
| 48 | +.back-icon { | |
| 49 | + width: 10px; | |
| 50 | + height: 10px; | |
| 51 | + margin-left: 4px; | |
| 52 | + border-left: 1.5px solid #fff; | |
| 53 | + border-bottom: 1.5px solid #fff; | |
| 54 | + transform: rotate(45deg); | |
| 55 | +} | |
| 56 | + | |
| 57 | +/* 首页图标 */ | |
| 58 | +.home-icon { | |
| 59 | + font-size: 20px; | |
| 60 | + font-weight: 600; | |
| 61 | + line-height: 1; | |
| 62 | +} | |
| 63 | + | |
| 64 | +.nav-bar-title { | |
| 65 | + position: absolute; | |
| 66 | + left: 50%; | |
| 67 | + top: 50%; | |
| 68 | + transform: translate(-50%, -50%); | |
| 69 | + font-size: 16px; | |
| 70 | + font-weight: bold; | |
| 71 | + max-width: 60%; | |
| 72 | + overflow: hidden; | |
| 73 | + text-overflow: ellipsis; | |
| 74 | + white-space: nowrap; | |
| 75 | + z-index: 1; | |
| 76 | +} | |
| 77 | + | |
| 78 | +.nav-bar-right { | |
| 79 | + display: flex; | |
| 80 | + align-items: center; | |
| 81 | + flex-shrink: 0; | |
| 82 | + min-width: 32px; | |
| 83 | + justify-content: flex-end; | |
| 84 | + z-index: 2; | |
| 85 | +} | |
| 86 | + | |
| 87 | +.nav-bar-placeholder { | |
| 88 | + width: 100%; | |
| 89 | +} | ... | ... |
pages/index/index.js
| 1 | -//index.js | |
| 2 | -//获取应用实例 | |
| 3 | -const app = getApp(); | |
| 4 | -const typeList = app.globalData.getTypeList(); | |
| 5 | -const selectedIndex = typeList.length > 1 ? app.globalData.getSelectedTypeIndex() : 0; | |
| 6 | - | |
| 7 | -let audio = null; | |
| 8 | -let videoAd = null; | |
| 9 | - | |
| 1 | +// index.js | |
| 10 | 2 | Page({ |
| 11 | - data: { | |
| 12 | - placeholder: '点击此处输入文字', | |
| 13 | - text: '', | |
| 14 | - typeList, | |
| 15 | - selectedIndex: String(selectedIndex) ? selectedIndex : 0, | |
| 16 | - isAudioPlaying: false, | |
| 17 | - currentAudioText: '' | |
| 18 | - }, | |
| 19 | - onLoad(){ | |
| 3 | + data: {}, | |
| 4 | + onLoad() { | |
| 20 | 5 | wx.showShareMenu(); |
| 21 | - this.initVideoAd(); | |
| 22 | - }, | |
| 23 | - | |
| 24 | - // 初始化激励视频广告 | |
| 25 | - initVideoAd() { | |
| 26 | - // 若在开发者工具中无法预览广告,请切换开发者工具中的基础库版本 | |
| 27 | - if (wx.createRewardedVideoAd) { | |
| 28 | - videoAd = wx.createRewardedVideoAd({ | |
| 29 | - adUnitId: 'adunit-aedf0933f69de926' | |
| 30 | - }); | |
| 31 | - | |
| 32 | - videoAd.onLoad(() => { | |
| 33 | - console.log('激励视频广告加载成功'); | |
| 34 | - }); | |
| 35 | - | |
| 36 | - videoAd.onError((err) => { | |
| 37 | - console.error('激励视频广告加载失败', err); | |
| 38 | - }); | |
| 39 | - | |
| 40 | - videoAd.onClose((res) => { | |
| 41 | - if (res && res.isEnded) { | |
| 42 | - // 用户看完广告,重置使用次数 | |
| 43 | - this.resetTtsCount(); | |
| 44 | - } else { | |
| 45 | - // 用户中途退出 | |
| 46 | - wx.showToast({ | |
| 47 | - title: '请看完广告获取次数', | |
| 48 | - icon: 'none' | |
| 49 | - }); | |
| 50 | - } | |
| 51 | - }); | |
| 52 | - } | |
| 53 | - }, | |
| 54 | - | |
| 55 | - // 显示激励视频广告 | |
| 56 | - showVideoAd() { | |
| 57 | - if (videoAd) { | |
| 58 | - videoAd.show().catch(() => { | |
| 59 | - // 失败重试 | |
| 60 | - videoAd.load() | |
| 61 | - .then(() => videoAd.show()) | |
| 62 | - .catch(err => { | |
| 63 | - console.error('激励视频广告显示失败', err); | |
| 64 | - wx.showToast({ | |
| 65 | - title: '广告加载失败,请稍后重试', | |
| 66 | - icon: 'none' | |
| 67 | - }); | |
| 68 | - }); | |
| 69 | - }); | |
| 70 | - } else { | |
| 71 | - wx.showToast({ | |
| 72 | - title: '广告功能暂不可用', | |
| 73 | - icon: 'none' | |
| 74 | - }); | |
| 75 | - } | |
| 76 | - }, | |
| 77 | - | |
| 78 | - // 重置TTS使用次数 | |
| 79 | - async resetTtsCount() { | |
| 80 | - wx.showLoading({ | |
| 81 | - title: '正在重置次数...', | |
| 82 | - mask: true | |
| 83 | - }); | |
| 84 | - | |
| 85 | - try { | |
| 86 | - const result = await new Promise((resolve, reject) => { | |
| 87 | - app.request({ | |
| 88 | - url: `${app.globalData.domain}/open-api/wx330e54aa6000516d/tts-count-reset`, | |
| 89 | - success(res) { | |
| 90 | - resolve(res.data); | |
| 91 | - }, | |
| 92 | - fail(err) { | |
| 93 | - reject(err); | |
| 94 | - } | |
| 95 | - }); | |
| 96 | - }); | |
| 97 | - | |
| 98 | - wx.hideLoading(); | |
| 99 | - | |
| 100 | - if (result.code === 2000000) { | |
| 101 | - wx.showToast({ | |
| 102 | - title: '获得10次使用机会!', | |
| 103 | - icon: 'success' | |
| 104 | - }); | |
| 105 | - } else { | |
| 106 | - wx.showToast({ | |
| 107 | - title: result.msg || '重置失败', | |
| 108 | - icon: 'none' | |
| 109 | - }); | |
| 110 | - } | |
| 111 | - } catch (error) { | |
| 112 | - wx.hideLoading(); | |
| 113 | - wx.showToast({ | |
| 114 | - title: '网络错误', | |
| 115 | - icon: 'none' | |
| 116 | - }); | |
| 117 | - } | |
| 118 | - }, | |
| 119 | - | |
| 120 | - // 显示次数用完弹窗 | |
| 121 | - showLimitDialog() { | |
| 122 | - wx.showModal({ | |
| 123 | - title: '使用次数已用完', | |
| 124 | - content: '今日免费次数已用完,观看广告可获得额外10次使用机会', | |
| 125 | - confirmText: '观看广告', | |
| 126 | - cancelText: '明日再试', | |
| 127 | - success: (res) => { | |
| 128 | - if (res.confirm) { | |
| 129 | - this.showVideoAd(); | |
| 130 | - } | |
| 131 | - } | |
| 132 | - }); | |
| 133 | - }, | |
| 134 | - inputText(e){ | |
| 135 | - this.data.text = e.detail.value; | |
| 136 | - }, | |
| 137 | - async play(){ | |
| 138 | - const text = this.data.text || this.data.placeholder; | |
| 139 | - const selectedType = this.data.typeList[this.data.selectedIndex]; | |
| 140 | - const type = selectedType.desc; | |
| 141 | - | |
| 142 | - this.resetAudio(); | |
| 143 | - audio = wx.createInnerAudioContext({useWebAudioImplement: true}); | |
| 144 | - audio.autoplay = true; | |
| 145 | - | |
| 146 | - // 显示播放蒙层 | |
| 147 | - this.setData({ | |
| 148 | - isAudioPlaying: true, | |
| 149 | - currentAudioText: text | |
| 150 | - }); | |
| 151 | - | |
| 152 | - // 初始化蒙层组件的音频监听 | |
| 153 | - setTimeout(() => { | |
| 154 | - const overlayComponent = this.selectComponent('audio-overlay'); | |
| 155 | - if (overlayComponent) { | |
| 156 | - overlayComponent.initAudio(audio); | |
| 157 | - } | |
| 158 | - }, 100); | |
| 159 | - | |
| 160 | - // 检查是否播放过 | |
| 161 | - const audioList = app.globalData.getAudioList(); | |
| 162 | - let played = audioList.find(v => v.text === text && v.type === type); | |
| 163 | - | |
| 164 | - // 播放过,直接使用本地文件,无需检查合规性 | |
| 165 | - if(played){ | |
| 166 | - played.time = Date.now(); | |
| 167 | - app.globalData.setAudioList('delete', played.path); | |
| 168 | - app.globalData.setAudioList('add', played); | |
| 169 | - // 播放失败要移除缓存内容 | |
| 170 | - audio.onError(err => { | |
| 171 | - app.globalData.setAudioList('delete', played.path); | |
| 172 | - this.setData({ isAudioPlaying: false }); | |
| 173 | - wx.showModal({ | |
| 174 | - title: '提示', | |
| 175 | - content: '该语音播放过但缓存失效,点击确定重新播放', | |
| 176 | - showCancel: false, | |
| 177 | - success: res => { | |
| 178 | - this.play(); | |
| 179 | - } | |
| 180 | - }); | |
| 181 | - }); | |
| 182 | - return audio.src = played.path; | |
| 183 | - } | |
| 184 | - | |
| 185 | - // 未播放过,先检查文本是否合规 | |
| 186 | - wx.showLoading({ | |
| 187 | - title: '正在合成语音...', | |
| 188 | - mask: true | |
| 189 | - }); | |
| 190 | - const result = await new Promise((resolve, reject) => { | |
| 191 | - app.request({ | |
| 192 | - url: `${app.globalData.domain}/open-api/mp/msg-sec-check`, | |
| 193 | - data: { | |
| 194 | - appid: 'wx330e54aa6000516d', | |
| 195 | - content: text, | |
| 196 | - openid: wx.getStorageSync('openid') | |
| 197 | - }, | |
| 198 | - success(res){ | |
| 199 | - resolve(JSON.parse(res.data?.data)?.result); | |
| 200 | - }, | |
| 201 | - fail(err){ | |
| 202 | - reject(err); | |
| 203 | - } | |
| 204 | - }); | |
| 205 | - }); | |
| 206 | - // 不合规结束 | |
| 207 | - if(result.suggest !== 'pass'){ | |
| 208 | - wx.hideLoading(); | |
| 209 | - this.setData({ isAudioPlaying: false }); | |
| 210 | - return wx.showToast({ | |
| 211 | - title: '内容不合规', | |
| 212 | - icon: 'error', | |
| 213 | - mask: true, | |
| 214 | - duration: 2000 | |
| 215 | - }); | |
| 216 | - } | |
| 217 | - // 未播放过,调用TTS接口生成语音 | |
| 218 | - try { | |
| 219 | - const url = await selectedType.createUrl(text); | |
| 220 | - // 下载并播放 | |
| 221 | - wx.downloadFile({ | |
| 222 | - url, | |
| 223 | - success (res) { | |
| 224 | - wx.hideLoading(); | |
| 225 | - audio.src = res.tempFilePath; | |
| 226 | - app.globalData.setAudioList('add', { | |
| 227 | - // 音频文本 | |
| 228 | - text, | |
| 229 | - // 音频音效 | |
| 230 | - type, | |
| 231 | - // 音频网络地址 | |
| 232 | - url, | |
| 233 | - // 音频本地地址 | |
| 234 | - path: res.tempFilePath, | |
| 235 | - // 音频创建时间 | |
| 236 | - time: Date.now() | |
| 237 | - }); | |
| 238 | - }, | |
| 239 | - fail(err) { | |
| 240 | - console.log(err); | |
| 241 | - wx.hideLoading(); | |
| 242 | - this.setData({ isAudioPlaying: false }); | |
| 243 | - wx.showToast({ | |
| 244 | - title: '下载失败', | |
| 245 | - icon: 'none' | |
| 246 | - }); | |
| 247 | - } | |
| 248 | - }); | |
| 249 | - } catch (error) { | |
| 250 | - wx.hideLoading(); | |
| 251 | - this.setData({ isAudioPlaying: false }); | |
| 252 | - // TTS接口调用失败的错误已经在app.js中处理了 | |
| 253 | - } | |
| 254 | - }, | |
| 255 | - clear(){ | |
| 256 | - this.resetAudio(); | |
| 257 | - this.setData({ | |
| 258 | - text: '' | |
| 259 | - }); | |
| 260 | - }, | |
| 261 | - resetAudio(){ | |
| 262 | - if(audio){ | |
| 263 | - audio.destroy(); | |
| 264 | - audio = null; | |
| 265 | - } | |
| 266 | - this.setData({ | |
| 267 | - isAudioPlaying: false, | |
| 268 | - currentAudioText: '' | |
| 269 | - }); | |
| 270 | - }, | |
| 271 | - | |
| 272 | - // 音频停止播放的回调 | |
| 273 | - onAudioStop() { | |
| 274 | - this.resetAudio(); | |
| 275 | - }, | |
| 276 | - selectIndex(e){ | |
| 277 | - const { index } = e.target.dataset; | |
| 278 | - this.setData({ | |
| 279 | - selectedIndex: index | |
| 280 | - }, () => { | |
| 281 | - this.play(); | |
| 282 | - }); | |
| 283 | - app.globalData.setSelectedTypeIndex(index); | |
| 284 | - }, | |
| 285 | - gotoAudioList(){ | |
| 286 | - wx.navigateTo({ | |
| 287 | - url: '/pages/result/result' | |
| 288 | - }); | |
| 289 | 6 | } |
| 290 | 7 | }); | ... | ... |
pages/index/index.wxml
| ... | ... | @@ -5,41 +5,12 @@ |
| 5 | 5 | <view class="ball ball-1"></view> |
| 6 | 6 | <view class="ball ball-2"></view> |
| 7 | 7 | </view> |
| 8 | - <view class="text-view"> | |
| 9 | - <textarea | |
| 10 | - auto-focus | |
| 11 | - class="text-area" | |
| 12 | - placeholder="{{placeholder}}" | |
| 13 | - confirm-type="done" | |
| 14 | - bindconfirm="play" | |
| 15 | - bindinput="inputText" | |
| 16 | - value="{{text}}" | |
| 17 | - /> | |
| 18 | - </view> | |
| 19 | - <view class="button-view"> | |
| 20 | - <button bindtap="play" class="button" type="primary" size="mini"> 播放 </button> | |
| 21 | - <button bindtap="gotoAudioList" class="goto-audio-list" type="primary" size="mini"> 下载 </button> | |
| 22 | - <button bindtap="clear" class="button" type="default" size="mini"> 清空 </button> | |
| 23 | - </view> | |
| 24 | - <view class="type-view" wx:if="{{typeList.length > 1}}"> | |
| 25 | - <button | |
| 26 | - wx:for="{{typeList}}" | |
| 27 | - wx:key="desc" | |
| 28 | - bindtap="selectIndex" | |
| 29 | - class="button" | |
| 30 | - data-index="{{index}}" | |
| 31 | - type="{{index === selectedIndex ? 'primary' : 'default'}}" | |
| 32 | - size="mini" | |
| 33 | - > {{item.desc}} </button> | |
| 8 | + | |
| 9 | + <!-- 主内容区 --> | |
| 10 | + <view class="main-content"> | |
| 11 | + <view class="title">欢迎使用文字转语音</view> | |
| 12 | + <navigator url="/pages/text-to-audio/text-to-audio" class="nav-button"> | |
| 13 | + 开始使用 | |
| 14 | + </navigator> | |
| 34 | 15 | </view> |
| 35 | 16 | </view> |
| 36 | - | |
| 37 | -<!-- 音频播放蒙层 --> | |
| 38 | -<audio-overlay | |
| 39 | - is-playing="{{isAudioPlaying}}" | |
| 40 | - audio-text="{{currentAudioText}}" | |
| 41 | - bind:stop="onAudioStop" | |
| 42 | -></audio-overlay> | |
| 43 | - | |
| 44 | -<!--ad class="ad-1" unit-id="adunit-34f63a98428c1660"></ad> | |
| 45 | -<ad class="ad-2" unit-id="adunit-80a27ecf9a35c553"></ad--> | |
| 46 | 17 | \ No newline at end of file | ... | ... |
pages/index/index.wxss
| ... | ... | @@ -27,14 +27,14 @@ page { |
| 27 | 27 | width: 400rpx; |
| 28 | 28 | height: 400rpx; |
| 29 | 29 | background-color: #4472c4; |
| 30 | - animation: move-1 5s infinite; | |
| 30 | + animation: move-1 8s ease-in-out infinite; | |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | 33 | .ball-2 { |
| 34 | 34 | width: 500rpx; |
| 35 | 35 | height: 500rpx; |
| 36 | 36 | background-color: #00b050; |
| 37 | - animation: move-2 5s infinite; | |
| 37 | + animation: move-2 10s ease-in-out infinite; | |
| 38 | 38 | } |
| 39 | 39 | |
| 40 | 40 | @keyframes move-1 { |
| ... | ... | @@ -42,13 +42,13 @@ page { |
| 42 | 42 | transform: translate(0, 0); |
| 43 | 43 | } |
| 44 | 44 | 25% { |
| 45 | - transform: translate(200rpx, 150rpx); | |
| 45 | + transform: translate(500rpx, 600rpx); | |
| 46 | 46 | } |
| 47 | 47 | 50% { |
| 48 | - transform: translate(0, 300rpx); | |
| 48 | + transform: translate(-300rpx, 1000rpx); | |
| 49 | 49 | } |
| 50 | 50 | 75% { |
| 51 | - transform: translate(-100rpx, 150rpx); | |
| 51 | + transform: translate(-400rpx, 400rpx); | |
| 52 | 52 | } |
| 53 | 53 | 100% { |
| 54 | 54 | transform: translate(0, 0); |
| ... | ... | @@ -57,234 +57,77 @@ page { |
| 57 | 57 | |
| 58 | 58 | @keyframes move-2 { |
| 59 | 59 | 0% { |
| 60 | - transform: translate(0, 0); | |
| 60 | + transform: translate(100rpx, 100rpx); | |
| 61 | 61 | } |
| 62 | 62 | 25% { |
| 63 | - transform: translate(-200rpx, -180rpx); | |
| 63 | + transform: translate(-500rpx, -200rpx); | |
| 64 | 64 | } |
| 65 | 65 | 50% { |
| 66 | - transform: translate(0, -320rpx); | |
| 66 | + transform: translate(400rpx, 800rpx); | |
| 67 | 67 | } |
| 68 | 68 | 75% { |
| 69 | - transform: translate(100rpx, -180rpx); | |
| 69 | + transform: translate(600rpx, 200rpx); | |
| 70 | 70 | } |
| 71 | 71 | 100% { |
| 72 | - transform: translate(0, 0); | |
| 72 | + transform: translate(100rpx, 100rpx); | |
| 73 | 73 | } |
| 74 | 74 | } |
| 75 | 75 | |
| 76 | -/* 全局移除按钮边框 */ | |
| 77 | -button { | |
| 78 | - border: none !important; | |
| 79 | - outline: none !important; | |
| 80 | -} | |
| 81 | - | |
| 82 | -button::after { | |
| 83 | - border: none !important; | |
| 84 | -} | |
| 85 | - | |
| 86 | -button::before { | |
| 87 | - border: none !important; | |
| 88 | -} | |
| 89 | - | |
| 90 | 76 | .container-custom { |
| 91 | 77 | position: relative; |
| 92 | 78 | width: 100%; |
| 93 | - max-width: 100vw; | |
| 94 | - overflow-x: hidden; | |
| 95 | - box-sizing: border-box; | |
| 96 | - padding: 20rpx; | |
| 97 | -} | |
| 98 | - | |
| 99 | -.text-view{ | |
| 100 | - width: 100%; | |
| 101 | - margin-bottom: 20rpx; | |
| 102 | - text-align: center; | |
| 79 | + min-height: 100vh; | |
| 80 | + display: flex; | |
| 81 | + align-items: center; | |
| 82 | + justify-content: center; | |
| 103 | 83 | box-sizing: border-box; |
| 104 | -} | |
| 105 | -.text-area{ | |
| 106 | - width: 100%; | |
| 107 | - max-width: 100%; | |
| 108 | 84 | padding: 20rpx; |
| 109 | - text-align: left; | |
| 110 | - box-sizing: border-box; | |
| 111 | - display: block; | |
| 112 | - border: none; | |
| 113 | - border-radius: 16rpx; | |
| 114 | - background-color: #fafafa; | |
| 115 | - font-size: 32rpx; | |
| 116 | - line-height: 1.5; | |
| 117 | - transition: all 0.3s ease; | |
| 118 | - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08); | |
| 119 | -} | |
| 120 | -.text-area:focus { | |
| 121 | - background-color: #ffffff; | |
| 122 | - box-shadow: 0 4rpx 20rpx rgba(0, 122, 255, 0.15); | |
| 123 | -} | |
| 124 | - | |
| 125 | -.button-view{ | |
| 126 | - width: 100%; | |
| 127 | - margin-bottom: 40rpx; | |
| 128 | - text-align: center; | |
| 129 | - box-sizing: border-box; | |
| 130 | 85 | } |
| 131 | 86 | |
| 132 | -/* 主要按钮样式 */ | |
| 133 | -.button-view button{ | |
| 134 | - width: 100%; | |
| 135 | - max-width: 100%; | |
| 136 | - display: block; | |
| 137 | - margin-bottom: 20rpx; | |
| 138 | - height: 88rpx; | |
| 139 | - line-height: 88rpx; | |
| 140 | - font-size: 32rpx; | |
| 141 | - text-align: center; | |
| 142 | - border-radius: 44rpx; | |
| 143 | - border: none !important; | |
| 144 | - font-weight: 600; | |
| 145 | - letter-spacing: 2rpx; | |
| 146 | - transition: all 0.3s ease; | |
| 87 | +/* 主内容区 */ | |
| 88 | +.main-content { | |
| 147 | 89 | position: relative; |
| 148 | - overflow: hidden; | |
| 149 | - box-sizing: border-box; | |
| 150 | -} | |
| 151 | - | |
| 152 | -.button-view button::after { | |
| 153 | - border: none !important; | |
| 154 | -} | |
| 155 | - | |
| 156 | -.button-view button::before { | |
| 157 | - border: none !important; | |
| 158 | -} | |
| 159 | - | |
| 160 | -/* 播放按钮 - 主要操作 */ | |
| 161 | -.button-view button[bindtap="play"] { | |
| 162 | - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 163 | - color: white; | |
| 164 | - box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4), inset 0 2rpx 0 rgba(255, 255, 255, 0.2); | |
| 165 | -} | |
| 166 | - | |
| 167 | -.button-view button[bindtap="play"]:active { | |
| 168 | - transform: translateY(2rpx); | |
| 169 | - box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.4); | |
| 170 | -} | |
| 171 | - | |
| 172 | -/* 下载按钮 - 次要操作 */ | |
| 173 | -.goto-audio-list { | |
| 174 | - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| 175 | - color: white; | |
| 176 | - box-shadow: 0 8rpx 24rpx rgba(240, 147, 251, 0.4), inset 0 2rpx 0 rgba(255, 255, 255, 0.2); | |
| 177 | -} | |
| 178 | - | |
| 179 | -.goto-audio-list:active { | |
| 180 | - transform: translateY(2rpx); | |
| 181 | - box-shadow: 0 4rpx 12rpx rgba(240, 147, 251, 0.4); | |
| 182 | -} | |
| 183 | - | |
| 184 | -/* 清空按钮 - 辅助操作 */ | |
| 185 | -.button-view button[bindtap="clear"] { | |
| 186 | - background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); | |
| 187 | - color: #8b4513; | |
| 188 | - box-shadow: 0 8rpx 24rpx rgba(252, 182, 159, 0.4), inset 0 2rpx 0 rgba(255, 255, 255, 0.3); | |
| 189 | -} | |
| 190 | - | |
| 191 | -.button-view button[bindtap="clear"]:active { | |
| 192 | - transform: translateY(2rpx); | |
| 193 | - box-shadow: 0 4rpx 12rpx rgba(252, 182, 159, 0.4); | |
| 194 | -} | |
| 195 | - | |
| 196 | -/* 语音类型选择区域 */ | |
| 197 | -.type-view{ | |
| 198 | - width: 100%; | |
| 199 | - max-width: 100%; | |
| 200 | - margin: 0 0 40rpx 0; | |
| 201 | - padding: 20rpx; | |
| 202 | - background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%); | |
| 203 | - border-radius: 20rpx; | |
| 204 | - box-shadow: 0 6rpx 24rpx rgba(0, 0, 0, 0.08); | |
| 205 | - backdrop-filter: blur(10rpx); | |
| 206 | - box-sizing: border-box; | |
| 207 | -} | |
| 208 | - | |
| 209 | -.type-view button{ | |
| 210 | - width: 100%; | |
| 211 | - max-width: 100%; | |
| 212 | - height: 60rpx; | |
| 213 | - margin-bottom: 16rpx; | |
| 214 | - padding: 0 20rpx; | |
| 215 | - writing-mode: horizontal-tb; | |
| 216 | - text-align: center; | |
| 217 | - letter-spacing: 1rpx; | |
| 218 | - font-size: 28rpx; | |
| 219 | - font-weight: 500; | |
| 220 | - border-radius: 30rpx; | |
| 221 | - border: none !important; | |
| 222 | - transition: all 0.3s ease; | |
| 223 | - position: relative; | |
| 224 | - overflow: hidden; | |
| 90 | + z-index: 2; | |
| 225 | 91 | display: flex; |
| 92 | + flex-direction: column; | |
| 226 | 93 | align-items: center; |
| 227 | 94 | justify-content: center; |
| 228 | - white-space: nowrap; | |
| 229 | - box-sizing: border-box; | |
| 230 | -} | |
| 231 | - | |
| 232 | -.type-view button::after { | |
| 233 | - border: none !important; | |
| 234 | -} | |
| 235 | - | |
| 236 | -.type-view button::before { | |
| 237 | - border: none !important; | |
| 238 | 95 | } |
| 239 | 96 | |
| 240 | -.type-view button:last-child { | |
| 241 | - margin-bottom: 0; | |
| 242 | -} | |
| 243 | - | |
| 244 | -/* 未选中的语音类型按钮 */ | |
| 245 | -.type-view button[type="default"] { | |
| 246 | - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); | |
| 247 | - color: #495057; | |
| 248 | - box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1), inset 0 1rpx 0 rgba(255, 255, 255, 0.8); | |
| 97 | +.title { | |
| 98 | + font-size: 48rpx; | |
| 99 | + font-weight: 600; | |
| 100 | + letter-spacing: 4rpx; | |
| 101 | + margin-bottom: 80rpx; | |
| 102 | + background: linear-gradient(45deg, #667eea, #764ba2, #f093fb, #f5576c); | |
| 103 | + background-size: 200% 100%; | |
| 104 | + -webkit-background-clip: text; | |
| 105 | + background-clip: text; | |
| 106 | + color: transparent; | |
| 107 | + animation: text-shine 3s linear infinite; | |
| 249 | 108 | } |
| 250 | 109 | |
| 251 | -.type-view button[type="default"]:active { | |
| 252 | - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); | |
| 253 | - transform: translateY(2rpx); | |
| 254 | - box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.15), inset 0 1rpx 0 rgba(255, 255, 255, 0.6); | |
| 110 | +@keyframes text-shine { | |
| 111 | + 0% { | |
| 112 | + background-position: 200% 0; | |
| 113 | + } | |
| 114 | + 100% { | |
| 115 | + background-position: -200% 0; | |
| 116 | + } | |
| 255 | 117 | } |
| 256 | 118 | |
| 257 | -/* 选中的语音类型按钮 */ | |
| 258 | -.type-view button[type="primary"] { | |
| 259 | - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 119 | +.nav-button { | |
| 120 | + padding: 24rpx 80rpx; | |
| 121 | + font-size: 32rpx; | |
| 122 | + font-weight: 600; | |
| 260 | 123 | color: white; |
| 261 | - box-shadow: 0 6rpx 20rpx rgba(102, 126, 234, 0.4), inset 0 1rpx 0 rgba(255, 255, 255, 0.2); | |
| 262 | - transform: scale(1.02); | |
| 263 | -} | |
| 264 | - | |
| 265 | -.type-view button[type="primary"]:active { | |
| 266 | - transform: scale(1.02) translateY(2rpx); | |
| 267 | - box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.5), inset 0 1rpx 0 rgba(255, 255, 255, 0.1); | |
| 268 | -} | |
| 269 | - | |
| 270 | -/* 按钮悬浮效果动画 */ | |
| 271 | -.button-view button::before, | |
| 272 | -.type-view button::before { | |
| 273 | - content: ''; | |
| 274 | - position: absolute; | |
| 275 | - top: 0; | |
| 276 | - left: -100%; | |
| 277 | - width: 100%; | |
| 278 | - height: 100%; | |
| 279 | - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); | |
| 280 | - transition: left 0.5s; | |
| 124 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 125 | + border-radius: 50rpx; | |
| 126 | + box-shadow: 0 12rpx 32rpx rgba(102, 126, 234, 0.4), inset 0 2rpx 0 rgba(255, 255, 255, 0.2); | |
| 127 | + transition: all 0.3s ease; | |
| 281 | 128 | } |
| 282 | 129 | |
| 283 | -.button-view button:active::before, | |
| 284 | -.type-view button:active::before { | |
| 285 | - left: 100%; | |
| 130 | +.nav-button:active { | |
| 131 | + transform: translateY(4rpx) scale(0.98); | |
| 132 | + box-shadow: 0 6rpx 16rpx rgba(102, 126, 234, 0.4); | |
| 286 | 133 | } |
| 287 | - | |
| 288 | -.ad-1{ | |
| 289 | - margin-bottom: 20rpx; | |
| 290 | -} | |
| 291 | 134 | \ No newline at end of file | ... | ... |
pages/result/result.wxml deleted
| 1 | -<!--view class="ad-placeholder"> | |
| 2 | - <ad unit-id="adunit-0a991618c53a4445" class="ad-3"></ad> | |
| 3 | -</view--> | |
| 4 | -<view | |
| 5 | - class="text-view" | |
| 6 | - wx:for="{{audioList}}" | |
| 7 | - wx:key="path" | |
| 8 | - > | |
| 9 | - <view | |
| 10 | - class="text"> | |
| 11 | - {{item.text}} | |
| 12 | - </view> | |
| 13 | - <view class="btn-view"> | |
| 14 | - <view class="text-time"><text class="text-type">{{item.type}}</text> - {{item.date}}</view> | |
| 15 | - <button class="button" type="warn" size="mini" data-path="{{item.path}}" bindtap="delete">删除</button> | |
| 16 | - <button class="button" type="primary" size="mini" data-path="{{item.path}}" bindtap="play">播放</button> | |
| 17 | - <button class="button" type="primary" size="mini" data-path="{{item.path}}" bindtap="download">下载</button> | |
| 18 | - </view> | |
| 19 | -</view> | |
| 20 | - | |
| 21 | -<!-- 音频播放蒙层 --> | |
| 22 | -<audio-overlay | |
| 23 | - is-playing="{{isAudioPlaying}}" | |
| 24 | - audio-text="{{currentAudioText}}" | |
| 25 | - bind:stop="onAudioStop" | |
| 26 | -></audio-overlay> | |
| 27 | 0 | \ No newline at end of file |
pages/result/result.wxss deleted
| 1 | -.text-view{ | |
| 2 | - margin-bottom: 10px; | |
| 3 | - padding: 10px; | |
| 4 | - background: #efefef; | |
| 5 | -} | |
| 6 | -.btn-view{ | |
| 7 | - padding-top: 10px; | |
| 8 | - text-align: right; | |
| 9 | - border-top: 0.5px solid #d0d0d0; | |
| 10 | -} | |
| 11 | -.button{ | |
| 12 | - margin-left: 10px; | |
| 13 | -} | |
| 14 | -.left{ | |
| 15 | - float: left; | |
| 16 | - margin-left: 0; | |
| 17 | -} | |
| 18 | -.text{ | |
| 19 | - padding-top: 10px; | |
| 20 | - padding-bottom: 20px; | |
| 21 | -} | |
| 22 | -.text-time{ | |
| 23 | - float: left; | |
| 24 | - font-size: 11px; | |
| 25 | - line-height: 2.6; | |
| 26 | - color: #999; | |
| 27 | -} | |
| 28 | -.text-type{ | |
| 29 | - color: #999; | |
| 30 | -} | |
| 31 | -.ad-placeholder{ | |
| 32 | - min-height: 150px; | |
| 33 | - margin-bottom: 10px; | |
| 34 | - background: #efefef; | |
| 35 | -} | |
| 36 | 0 | \ No newline at end of file |
pages/result/result.js renamed to pages/text-to-audio-result/text-to-audio-result.js
pages/result/result.json renamed to pages/text-to-audio-result/text-to-audio-result.json
pages/text-to-audio-result/text-to-audio-result.wxml
0 → 100644
| 1 | +<!--text-to-audio-result.wxml--> | |
| 2 | +<view class="container-custom" style="padding-top: 0; padding-bottom: 0;"> | |
| 3 | + <!-- 自定义导航栏 --> | |
| 4 | + <custom-nav-bar | |
| 5 | + showBack="{{true}}" | |
| 6 | + noPlaceholder="{{true}}" | |
| 7 | + /> | |
| 8 | + | |
| 9 | + <!-- 动态背景 --> | |
| 10 | + <view class="bg"> | |
| 11 | + <view class="ball ball-1"></view> | |
| 12 | + <view class="ball ball-2"></view> | |
| 13 | + </view> | |
| 14 | + | |
| 15 | + <!-- 主内容区 --> | |
| 16 | + <view class="container-default-box"> | |
| 17 | + <view class="audio-card" wx:for="{{audioList}}" wx:key="path"> | |
| 18 | + <view class="audio-text">{{item.text}}</view> | |
| 19 | + <view class="audio-footer"> | |
| 20 | + <view class="audio-info"> | |
| 21 | + <text class="audio-type">{{item.type}}</text> | |
| 22 | + <text class="audio-date">{{item.date}}</text> | |
| 23 | + </view> | |
| 24 | + <view class="audio-actions"> | |
| 25 | + <button class="btn-action btn-play" size="mini" data-path="{{item.path}}" bindtap="play">播放</button> | |
| 26 | + <button class="btn-action btn-download" size="mini" data-path="{{item.path}}" bindtap="download">下载</button> | |
| 27 | + <button class="btn-action btn-delete" size="mini" data-path="{{item.path}}" bindtap="delete">删除</button> | |
| 28 | + </view> | |
| 29 | + </view> | |
| 30 | + </view> | |
| 31 | + | |
| 32 | + <view class="empty-state" wx:if="{{!audioList || audioList.length === 0}}"> | |
| 33 | + <view class="empty-icon">🎵</view> | |
| 34 | + <view class="empty-text">还没有音频</view> | |
| 35 | + <view class="empty-desc">去生成一些语音吧</view> | |
| 36 | + </view> | |
| 37 | + </view> | |
| 38 | +</view> | |
| 39 | + | |
| 40 | +<!-- 音频播放蒙层 --> | |
| 41 | +<audio-overlay | |
| 42 | + is-playing="{{isAudioPlaying}}" | |
| 43 | + audio-text="{{currentAudioText}}" | |
| 44 | + bind:stop="onAudioStop" | |
| 45 | +></audio-overlay> | ... | ... |
pages/text-to-audio-result/text-to-audio-result.wxss
0 → 100644
| 1 | +/**text-to-audio-result.wxss**/ | |
| 2 | + | |
| 3 | +/* 防止横向滚动 */ | |
| 4 | +page { | |
| 5 | + width: 100%; | |
| 6 | + height: 100%; | |
| 7 | + overflow-x: hidden; | |
| 8 | + box-sizing: border-box; | |
| 9 | +} | |
| 10 | + | |
| 11 | +/* 动态背景样式 */ | |
| 12 | +.bg { | |
| 13 | + position: fixed; | |
| 14 | + top: 0; | |
| 15 | + left: 0; | |
| 16 | + width: 100%; | |
| 17 | + height: 100%; | |
| 18 | + filter: blur(120px); | |
| 19 | + z-index: -1; | |
| 20 | +} | |
| 21 | + | |
| 22 | +.ball { | |
| 23 | + position: absolute; | |
| 24 | + border-radius: 50%; | |
| 25 | +} | |
| 26 | + | |
| 27 | +.ball-1 { | |
| 28 | + width: 400rpx; | |
| 29 | + height: 400rpx; | |
| 30 | + background-color: #4472c4; | |
| 31 | + animation: move-1 8s ease-in-out infinite; | |
| 32 | +} | |
| 33 | + | |
| 34 | +.ball-2 { | |
| 35 | + width: 500rpx; | |
| 36 | + height: 500rpx; | |
| 37 | + background-color: #00b050; | |
| 38 | + animation: move-2 10s ease-in-out infinite; | |
| 39 | +} | |
| 40 | + | |
| 41 | +@keyframes move-1 { | |
| 42 | + 0% { | |
| 43 | + transform: translate(0, 0); | |
| 44 | + } | |
| 45 | + 25% { | |
| 46 | + transform: translate(500rpx, 600rpx); | |
| 47 | + } | |
| 48 | + 50% { | |
| 49 | + transform: translate(-300rpx, 1000rpx); | |
| 50 | + } | |
| 51 | + 75% { | |
| 52 | + transform: translate(-400rpx, 400rpx); | |
| 53 | + } | |
| 54 | + 100% { | |
| 55 | + transform: translate(0, 0); | |
| 56 | + } | |
| 57 | +} | |
| 58 | + | |
| 59 | +@keyframes move-2 { | |
| 60 | + 0% { | |
| 61 | + transform: translate(100rpx, 100rpx); | |
| 62 | + } | |
| 63 | + 25% { | |
| 64 | + transform: translate(-500rpx, -200rpx); | |
| 65 | + } | |
| 66 | + 50% { | |
| 67 | + transform: translate(400rpx, 800rpx); | |
| 68 | + } | |
| 69 | + 75% { | |
| 70 | + transform: translate(600rpx, 200rpx); | |
| 71 | + } | |
| 72 | + 100% { | |
| 73 | + transform: translate(100rpx, 100rpx); | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +/* 全局移除按钮边框 */ | |
| 78 | +button { | |
| 79 | + border: none !important; | |
| 80 | + outline: none !important; | |
| 81 | +} | |
| 82 | + | |
| 83 | +button::after { | |
| 84 | + border: none !important; | |
| 85 | +} | |
| 86 | + | |
| 87 | +button::before { | |
| 88 | + border: none !important; | |
| 89 | +} | |
| 90 | + | |
| 91 | +/* 音频卡片 */ | |
| 92 | +.audio-card { | |
| 93 | + margin-bottom: 20rpx; | |
| 94 | + padding: 30rpx; | |
| 95 | + background: rgba(255, 255, 255, 0.95); | |
| 96 | + backdrop-filter: blur(10rpx); | |
| 97 | + border-radius: 24rpx; | |
| 98 | + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08); | |
| 99 | + transition: all 0.3s ease; | |
| 100 | +} | |
| 101 | + | |
| 102 | +.audio-card:active { | |
| 103 | + transform: translateY(-2rpx); | |
| 104 | + box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.12); | |
| 105 | +} | |
| 106 | + | |
| 107 | +.audio-text { | |
| 108 | + font-size: 32rpx; | |
| 109 | + line-height: 1.6; | |
| 110 | + color: #333; | |
| 111 | + margin-bottom: 24rpx; | |
| 112 | + word-break: break-all; | |
| 113 | +} | |
| 114 | + | |
| 115 | +.audio-footer { | |
| 116 | + display: flex; | |
| 117 | + flex-direction: column; | |
| 118 | + gap: 20rpx; | |
| 119 | +} | |
| 120 | + | |
| 121 | +.audio-info { | |
| 122 | + display: flex; | |
| 123 | + align-items: center; | |
| 124 | + gap: 16rpx; | |
| 125 | + font-size: 24rpx; | |
| 126 | + color: #999; | |
| 127 | +} | |
| 128 | + | |
| 129 | +.audio-type { | |
| 130 | + padding: 4rpx 12rpx; | |
| 131 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 132 | + color: white; | |
| 133 | + border-radius: 8rpx; | |
| 134 | + font-size: 22rpx; | |
| 135 | +} | |
| 136 | + | |
| 137 | +.audio-date { | |
| 138 | + color: #999; | |
| 139 | +} | |
| 140 | + | |
| 141 | +.audio-actions { | |
| 142 | + display: flex; | |
| 143 | + gap: 16rpx; | |
| 144 | +} | |
| 145 | + | |
| 146 | +.btn-action { | |
| 147 | + flex: 1; | |
| 148 | + height: 64rpx; | |
| 149 | + line-height: 64rpx; | |
| 150 | + font-size: 26rpx; | |
| 151 | + border-radius: 32rpx; | |
| 152 | + transition: all 0.3s ease; | |
| 153 | +} | |
| 154 | + | |
| 155 | +.btn-play { | |
| 156 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 157 | + color: white; | |
| 158 | + box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4); | |
| 159 | +} | |
| 160 | + | |
| 161 | +.btn-play:active { | |
| 162 | + transform: scale(0.95); | |
| 163 | + box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.4); | |
| 164 | +} | |
| 165 | + | |
| 166 | +.btn-download { | |
| 167 | + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| 168 | + color: white; | |
| 169 | + box-shadow: 0 4rpx 16rpx rgba(240, 147, 251, 0.4); | |
| 170 | +} | |
| 171 | + | |
| 172 | +.btn-download:active { | |
| 173 | + transform: scale(0.95); | |
| 174 | + box-shadow: 0 2rpx 8rpx rgba(240, 147, 251, 0.4); | |
| 175 | +} | |
| 176 | + | |
| 177 | +.btn-delete { | |
| 178 | + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%); | |
| 179 | + color: white; | |
| 180 | + box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.4); | |
| 181 | +} | |
| 182 | + | |
| 183 | +.btn-delete:active { | |
| 184 | + transform: scale(0.95); | |
| 185 | + box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.4); | |
| 186 | +} | |
| 187 | + | |
| 188 | +/* 空状态 */ | |
| 189 | +.empty-state { | |
| 190 | + padding: 120rpx 60rpx; | |
| 191 | + text-align: center; | |
| 192 | +} | |
| 193 | + | |
| 194 | +.empty-icon { | |
| 195 | + font-size: 120rpx; | |
| 196 | + margin-bottom: 32rpx; | |
| 197 | + opacity: 0.6; | |
| 198 | +} | |
| 199 | + | |
| 200 | +.empty-text { | |
| 201 | + font-size: 32rpx; | |
| 202 | + color: #666; | |
| 203 | + margin-bottom: 16rpx; | |
| 204 | + font-weight: 600; | |
| 205 | +} | |
| 206 | + | |
| 207 | +.empty-desc { | |
| 208 | + font-size: 28rpx; | |
| 209 | + color: #999; | |
| 210 | +} | ... | ... |
pages/text-to-audio/text-to-audio.js
0 → 100644
| 1 | +//text-to-audio.js | |
| 2 | +//获取应用实例 | |
| 3 | +const app = getApp(); | |
| 4 | +const typeList = app.globalData.getTypeList(); | |
| 5 | +const selectedIndex = typeList.length > 1 ? app.globalData.getSelectedTypeIndex() : 0; | |
| 6 | + | |
| 7 | +let audio = null; | |
| 8 | +let videoAd = null; | |
| 9 | + | |
| 10 | +Page({ | |
| 11 | + data: { | |
| 12 | + placeholder: '点击此处输入文字', | |
| 13 | + text: '', | |
| 14 | + typeList, | |
| 15 | + selectedIndex: String(selectedIndex) ? selectedIndex : 0, | |
| 16 | + isAudioPlaying: false, | |
| 17 | + currentAudioText: '' | |
| 18 | + }, | |
| 19 | + onLoad(){ | |
| 20 | + wx.showShareMenu(); | |
| 21 | + this.initVideoAd(); | |
| 22 | + }, | |
| 23 | + | |
| 24 | + // 初始化激励视频广告 | |
| 25 | + initVideoAd() { | |
| 26 | + // 若在开发者工具中无法预览广告,请切换开发者工具中的基础库版本 | |
| 27 | + if (wx.createRewardedVideoAd) { | |
| 28 | + videoAd = wx.createRewardedVideoAd({ | |
| 29 | + adUnitId: 'adunit-aedf0933f69de926' | |
| 30 | + }); | |
| 31 | + | |
| 32 | + videoAd.onLoad(() => { | |
| 33 | + console.log('激励视频广告加载成功'); | |
| 34 | + }); | |
| 35 | + | |
| 36 | + videoAd.onError((err) => { | |
| 37 | + console.error('激励视频广告加载失败', err); | |
| 38 | + }); | |
| 39 | + | |
| 40 | + videoAd.onClose((res) => { | |
| 41 | + if (res && res.isEnded) { | |
| 42 | + // 用户看完广告,重置使用次数 | |
| 43 | + this.resetTtsCount(); | |
| 44 | + } else { | |
| 45 | + // 用户中途退出 | |
| 46 | + wx.showToast({ | |
| 47 | + title: '请看完广告获取次数', | |
| 48 | + icon: 'none' | |
| 49 | + }); | |
| 50 | + } | |
| 51 | + }); | |
| 52 | + } | |
| 53 | + }, | |
| 54 | + | |
| 55 | + // 显示激励视频广告 | |
| 56 | + showVideoAd() { | |
| 57 | + if (videoAd) { | |
| 58 | + videoAd.show().catch(() => { | |
| 59 | + // 失败重试 | |
| 60 | + videoAd.load() | |
| 61 | + .then(() => videoAd.show()) | |
| 62 | + .catch(err => { | |
| 63 | + console.error('激励视频广告显示失败', err); | |
| 64 | + wx.showToast({ | |
| 65 | + title: '广告加载失败,请稍后重试', | |
| 66 | + icon: 'none' | |
| 67 | + }); | |
| 68 | + }); | |
| 69 | + }); | |
| 70 | + } else { | |
| 71 | + wx.showToast({ | |
| 72 | + title: '广告功能暂不可用', | |
| 73 | + icon: 'none' | |
| 74 | + }); | |
| 75 | + } | |
| 76 | + }, | |
| 77 | + | |
| 78 | + // 重置TTS使用次数 | |
| 79 | + async resetTtsCount() { | |
| 80 | + wx.showLoading({ | |
| 81 | + title: '正在重置次数...', | |
| 82 | + mask: true | |
| 83 | + }); | |
| 84 | + | |
| 85 | + try { | |
| 86 | + const result = await new Promise((resolve, reject) => { | |
| 87 | + app.request({ | |
| 88 | + url: `${app.globalData.domain}/open-api/wx330e54aa6000516d/tts-count-reset`, | |
| 89 | + success(res) { | |
| 90 | + resolve(res.data); | |
| 91 | + }, | |
| 92 | + fail(err) { | |
| 93 | + reject(err); | |
| 94 | + } | |
| 95 | + }); | |
| 96 | + }); | |
| 97 | + | |
| 98 | + wx.hideLoading(); | |
| 99 | + | |
| 100 | + if (result.code === 2000000) { | |
| 101 | + wx.showToast({ | |
| 102 | + title: '获得10次使用机会!', | |
| 103 | + icon: 'success' | |
| 104 | + }); | |
| 105 | + } else { | |
| 106 | + wx.showToast({ | |
| 107 | + title: result.msg || '重置失败', | |
| 108 | + icon: 'none' | |
| 109 | + }); | |
| 110 | + } | |
| 111 | + } catch (error) { | |
| 112 | + wx.hideLoading(); | |
| 113 | + wx.showToast({ | |
| 114 | + title: '网络错误', | |
| 115 | + icon: 'none' | |
| 116 | + }); | |
| 117 | + } | |
| 118 | + }, | |
| 119 | + | |
| 120 | + // 显示次数用完弹窗 | |
| 121 | + showLimitDialog() { | |
| 122 | + wx.showModal({ | |
| 123 | + title: '使用次数已用完', | |
| 124 | + content: '今日免费次数已用完,观看广告可获得额外10次使用机会', | |
| 125 | + confirmText: '观看广告', | |
| 126 | + cancelText: '明日再试', | |
| 127 | + success: (res) => { | |
| 128 | + if (res.confirm) { | |
| 129 | + this.showVideoAd(); | |
| 130 | + } | |
| 131 | + } | |
| 132 | + }); | |
| 133 | + }, | |
| 134 | + inputText(e){ | |
| 135 | + this.data.text = e.detail.value; | |
| 136 | + }, | |
| 137 | + async play(){ | |
| 138 | + const text = this.data.text || this.data.placeholder; | |
| 139 | + const selectedType = this.data.typeList[this.data.selectedIndex]; | |
| 140 | + const type = selectedType.desc; | |
| 141 | + | |
| 142 | + this.resetAudio(); | |
| 143 | + audio = wx.createInnerAudioContext({useWebAudioImplement: true}); | |
| 144 | + audio.autoplay = true; | |
| 145 | + | |
| 146 | + // 显示播放蒙层 | |
| 147 | + this.setData({ | |
| 148 | + isAudioPlaying: true, | |
| 149 | + currentAudioText: text | |
| 150 | + }); | |
| 151 | + | |
| 152 | + // 初始化蒙层组件的音频监听 | |
| 153 | + setTimeout(() => { | |
| 154 | + const overlayComponent = this.selectComponent('audio-overlay'); | |
| 155 | + if (overlayComponent) { | |
| 156 | + overlayComponent.initAudio(audio); | |
| 157 | + } | |
| 158 | + }, 100); | |
| 159 | + | |
| 160 | + // 检查是否播放过 | |
| 161 | + const audioList = app.globalData.getAudioList(); | |
| 162 | + let played = audioList.find(v => v.text === text && v.type === type); | |
| 163 | + | |
| 164 | + // 播放过,直接使用本地文件,无需检查合规性 | |
| 165 | + if(played){ | |
| 166 | + played.time = Date.now(); | |
| 167 | + app.globalData.setAudioList('delete', played.path); | |
| 168 | + app.globalData.setAudioList('add', played); | |
| 169 | + // 播放失败要移除缓存内容 | |
| 170 | + audio.onError(err => { | |
| 171 | + app.globalData.setAudioList('delete', played.path); | |
| 172 | + this.setData({ isAudioPlaying: false }); | |
| 173 | + wx.showModal({ | |
| 174 | + title: '提示', | |
| 175 | + content: '该语音播放过但缓存失效,点击确定重新播放', | |
| 176 | + showCancel: false, | |
| 177 | + success: res => { | |
| 178 | + this.play(); | |
| 179 | + } | |
| 180 | + }); | |
| 181 | + }); | |
| 182 | + return audio.src = played.path; | |
| 183 | + } | |
| 184 | + | |
| 185 | + // 未播放过,先检查文本是否合规 | |
| 186 | + wx.showLoading({ | |
| 187 | + title: '正在合成语音...', | |
| 188 | + mask: true | |
| 189 | + }); | |
| 190 | + const result = await new Promise((resolve, reject) => { | |
| 191 | + app.request({ | |
| 192 | + url: `${app.globalData.domain}/open-api/mp/msg-sec-check`, | |
| 193 | + data: { | |
| 194 | + appid: 'wx330e54aa6000516d', | |
| 195 | + content: text, | |
| 196 | + openid: wx.getStorageSync('openid') | |
| 197 | + }, | |
| 198 | + success(res){ | |
| 199 | + resolve(JSON.parse(res.data?.data)?.result); | |
| 200 | + }, | |
| 201 | + fail(err){ | |
| 202 | + reject(err); | |
| 203 | + } | |
| 204 | + }); | |
| 205 | + }); | |
| 206 | + // 不合规结束 | |
| 207 | + if(result.suggest !== 'pass'){ | |
| 208 | + wx.hideLoading(); | |
| 209 | + this.setData({ isAudioPlaying: false }); | |
| 210 | + return wx.showToast({ | |
| 211 | + title: '内容不合规', | |
| 212 | + icon: 'error', | |
| 213 | + mask: true, | |
| 214 | + duration: 2000 | |
| 215 | + }); | |
| 216 | + } | |
| 217 | + // 未播放过,调用TTS接口生成语音 | |
| 218 | + try { | |
| 219 | + const url = await selectedType.createUrl(text); | |
| 220 | + // 下载并播放 | |
| 221 | + wx.downloadFile({ | |
| 222 | + url, | |
| 223 | + success (res) { | |
| 224 | + wx.hideLoading(); | |
| 225 | + audio.src = res.tempFilePath; | |
| 226 | + app.globalData.setAudioList('add', { | |
| 227 | + // 音频文本 | |
| 228 | + text, | |
| 229 | + // 音频音效 | |
| 230 | + type, | |
| 231 | + // 音频网络地址 | |
| 232 | + url, | |
| 233 | + // 音频本地地址 | |
| 234 | + path: res.tempFilePath, | |
| 235 | + // 音频创建时间 | |
| 236 | + time: Date.now() | |
| 237 | + }); | |
| 238 | + }, | |
| 239 | + fail(err) { | |
| 240 | + console.log(err); | |
| 241 | + wx.hideLoading(); | |
| 242 | + this.setData({ isAudioPlaying: false }); | |
| 243 | + wx.showToast({ | |
| 244 | + title: '下载失败', | |
| 245 | + icon: 'none' | |
| 246 | + }); | |
| 247 | + } | |
| 248 | + }); | |
| 249 | + } catch (error) { | |
| 250 | + wx.hideLoading(); | |
| 251 | + this.setData({ isAudioPlaying: false }); | |
| 252 | + // TTS接口调用失败的错误已经在app.js中处理了 | |
| 253 | + } | |
| 254 | + }, | |
| 255 | + clear(){ | |
| 256 | + this.resetAudio(); | |
| 257 | + this.setData({ | |
| 258 | + text: '' | |
| 259 | + }); | |
| 260 | + }, | |
| 261 | + resetAudio(){ | |
| 262 | + if(audio){ | |
| 263 | + audio.destroy(); | |
| 264 | + audio = null; | |
| 265 | + } | |
| 266 | + this.setData({ | |
| 267 | + isAudioPlaying: false, | |
| 268 | + currentAudioText: '' | |
| 269 | + }); | |
| 270 | + }, | |
| 271 | + | |
| 272 | + // 音频停止播放的回调 | |
| 273 | + onAudioStop() { | |
| 274 | + this.resetAudio(); | |
| 275 | + }, | |
| 276 | + selectIndex(e){ | |
| 277 | + const { index } = e.target.dataset; | |
| 278 | + this.setData({ | |
| 279 | + selectedIndex: index | |
| 280 | + }, () => { | |
| 281 | + this.play(); | |
| 282 | + }); | |
| 283 | + app.globalData.setSelectedTypeIndex(index); | |
| 284 | + }, | |
| 285 | + gotoAudioList(){ | |
| 286 | + wx.navigateTo({ | |
| 287 | + url: '/pages/text-to-audio-result/text-to-audio-result' | |
| 288 | + }); | |
| 289 | + } | |
| 290 | +}); | ... | ... |
pages/text-to-audio/text-to-audio.json
0 → 100644
pages/text-to-audio/text-to-audio.wxml
0 → 100644
| 1 | +<!--text-to-audio.wxml--> | |
| 2 | +<view class="container-custom" style="padding-top: 0; padding-bottom: 0;"> | |
| 3 | + <!-- 自定义导航栏 --> | |
| 4 | + <custom-nav-bar | |
| 5 | + showBack="{{true}}" | |
| 6 | + noPlaceholder="{{true}}" | |
| 7 | + /> | |
| 8 | + | |
| 9 | + <!-- 动态背景 --> | |
| 10 | + <view class="bg"> | |
| 11 | + <view class="ball ball-1"></view> | |
| 12 | + <view class="ball ball-2"></view> | |
| 13 | + </view> | |
| 14 | + | |
| 15 | + <!-- 主内容区 --> | |
| 16 | + <view class="container-default-box"> | |
| 17 | + <view class="text-view"> | |
| 18 | + <textarea | |
| 19 | + auto-focus | |
| 20 | + class="text-area" | |
| 21 | + placeholder="{{placeholder}}" | |
| 22 | + confirm-type="done" | |
| 23 | + bindconfirm="play" | |
| 24 | + bindinput="inputText" | |
| 25 | + value="{{text}}" | |
| 26 | + /> | |
| 27 | + </view> | |
| 28 | + <view class="button-view"> | |
| 29 | + <button bindtap="play" class="btn-play"> | |
| 30 | + <text class="btn-icon">▶</text> 播放 | |
| 31 | + </button> | |
| 32 | + <button bindtap="gotoAudioList" class="btn-download"> | |
| 33 | + <text class="btn-icon">↓</text> 下载 | |
| 34 | + </button> | |
| 35 | + <button bindtap="clear" class="btn-clear"> | |
| 36 | + <text class="btn-icon">✕</text> 清空 | |
| 37 | + </button> | |
| 38 | + </view> | |
| 39 | + <view class="type-view" wx:if="{{typeList.length > 1}}"> | |
| 40 | + <view class="type-title">选择语音类型</view> | |
| 41 | + <button | |
| 42 | + wx:for="{{typeList}}" | |
| 43 | + wx:key="desc" | |
| 44 | + bindtap="selectIndex" | |
| 45 | + class="type-button {{index === selectedIndex ? 'selected' : ''}}" | |
| 46 | + data-index="{{index}}" | |
| 47 | + > {{item.desc}} </button> | |
| 48 | + </view> | |
| 49 | + </view> | |
| 50 | +</view> | |
| 51 | + | |
| 52 | +<!-- 音频播放蒙层 --> | |
| 53 | +<audio-overlay | |
| 54 | + is-playing="{{isAudioPlaying}}" | |
| 55 | + audio-text="{{currentAudioText}}" | |
| 56 | + bind:stop="onAudioStop" | |
| 57 | +></audio-overlay> | ... | ... |
pages/text-to-audio/text-to-audio.wxss
0 → 100644
| 1 | +/**text-to-audio.wxss**/ | |
| 2 | + | |
| 3 | +/* 防止横向滚动 */ | |
| 4 | +page { | |
| 5 | + width: 100%; | |
| 6 | + height: 100%; | |
| 7 | + overflow-x: hidden; | |
| 8 | + box-sizing: border-box; | |
| 9 | +} | |
| 10 | + | |
| 11 | +/* 动态背景样式 */ | |
| 12 | +.bg { | |
| 13 | + position: fixed; | |
| 14 | + top: 0; | |
| 15 | + left: 0; | |
| 16 | + width: 100%; | |
| 17 | + height: 100%; | |
| 18 | + filter: blur(120px); | |
| 19 | + z-index: -1; | |
| 20 | +} | |
| 21 | + | |
| 22 | +.ball { | |
| 23 | + position: absolute; | |
| 24 | + border-radius: 50%; | |
| 25 | +} | |
| 26 | + | |
| 27 | +.ball-1 { | |
| 28 | + width: 400rpx; | |
| 29 | + height: 400rpx; | |
| 30 | + background-color: #4472c4; | |
| 31 | + animation: move-1 8s ease-in-out infinite; | |
| 32 | +} | |
| 33 | + | |
| 34 | +.ball-2 { | |
| 35 | + width: 500rpx; | |
| 36 | + height: 500rpx; | |
| 37 | + background-color: #00b050; | |
| 38 | + animation: move-2 10s ease-in-out infinite; | |
| 39 | +} | |
| 40 | + | |
| 41 | +@keyframes move-1 { | |
| 42 | + 0% { | |
| 43 | + transform: translate(0, 0); | |
| 44 | + } | |
| 45 | + 25% { | |
| 46 | + transform: translate(500rpx, 600rpx); | |
| 47 | + } | |
| 48 | + 50% { | |
| 49 | + transform: translate(-300rpx, 1000rpx); | |
| 50 | + } | |
| 51 | + 75% { | |
| 52 | + transform: translate(-400rpx, 400rpx); | |
| 53 | + } | |
| 54 | + 100% { | |
| 55 | + transform: translate(0, 0); | |
| 56 | + } | |
| 57 | +} | |
| 58 | + | |
| 59 | +@keyframes move-2 { | |
| 60 | + 0% { | |
| 61 | + transform: translate(100rpx, 100rpx); | |
| 62 | + } | |
| 63 | + 25% { | |
| 64 | + transform: translate(-500rpx, -200rpx); | |
| 65 | + } | |
| 66 | + 50% { | |
| 67 | + transform: translate(400rpx, 800rpx); | |
| 68 | + } | |
| 69 | + 75% { | |
| 70 | + transform: translate(600rpx, 200rpx); | |
| 71 | + } | |
| 72 | + 100% { | |
| 73 | + transform: translate(100rpx, 100rpx); | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +/* 全局移除按钮边框 */ | |
| 78 | +button { | |
| 79 | + border: none !important; | |
| 80 | + outline: none !important; | |
| 81 | +} | |
| 82 | + | |
| 83 | +button::after { | |
| 84 | + border: none !important; | |
| 85 | +} | |
| 86 | + | |
| 87 | +button::before { | |
| 88 | + border: none !important; | |
| 89 | +} | |
| 90 | + | |
| 91 | +/* 文本输入区 */ | |
| 92 | +.text-view { | |
| 93 | + width: 100%; | |
| 94 | + margin-bottom: 30rpx; | |
| 95 | +} | |
| 96 | + | |
| 97 | +.text-area { | |
| 98 | + width: 100%; | |
| 99 | + min-height: 300rpx; | |
| 100 | + padding: 30rpx; | |
| 101 | + box-sizing: border-box; | |
| 102 | + border-radius: 24rpx; | |
| 103 | + background: rgba(255, 255, 255, 0.95); | |
| 104 | + backdrop-filter: blur(10rpx); | |
| 105 | + font-size: 32rpx; | |
| 106 | + line-height: 1.6; | |
| 107 | + color: #333; | |
| 108 | + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08); | |
| 109 | + transition: all 0.3s ease; | |
| 110 | +} | |
| 111 | + | |
| 112 | +.text-area:focus { | |
| 113 | + background: rgba(255, 255, 255, 1); | |
| 114 | + box-shadow: 0 12rpx 48rpx rgba(102, 126, 234, 0.15); | |
| 115 | + transform: translateY(-4rpx); | |
| 116 | +} | |
| 117 | + | |
| 118 | +/* 按钮区域 */ | |
| 119 | +.button-view { | |
| 120 | + display: flex; | |
| 121 | + gap: 20rpx; | |
| 122 | + margin-bottom: 30rpx; | |
| 123 | +} | |
| 124 | + | |
| 125 | +.button-view button { | |
| 126 | + flex: 1; | |
| 127 | + height: 96rpx; | |
| 128 | + display: flex; | |
| 129 | + align-items: center; | |
| 130 | + justify-content: center; | |
| 131 | + border-radius: 48rpx; | |
| 132 | + font-size: 30rpx; | |
| 133 | + font-weight: 600; | |
| 134 | + transition: all 0.3s ease; | |
| 135 | + position: relative; | |
| 136 | + overflow: visible; | |
| 137 | +} | |
| 138 | + | |
| 139 | +.btn-icon { | |
| 140 | + margin-right: 8rpx; | |
| 141 | + font-size: 28rpx; | |
| 142 | +} | |
| 143 | + | |
| 144 | +/* 播放按钮 */ | |
| 145 | +.btn-play { | |
| 146 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 147 | + color: white; | |
| 148 | + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4); | |
| 149 | +} | |
| 150 | + | |
| 151 | +.btn-play:active { | |
| 152 | + transform: translateY(4rpx) scale(0.98); | |
| 153 | + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.4); | |
| 154 | +} | |
| 155 | + | |
| 156 | +/* 下载按钮 */ | |
| 157 | +.btn-download { | |
| 158 | + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| 159 | + color: white; | |
| 160 | + box-shadow: 0 8rpx 24rpx rgba(240, 147, 251, 0.4); | |
| 161 | +} | |
| 162 | + | |
| 163 | +.btn-download:active { | |
| 164 | + transform: translateY(4rpx) scale(0.98); | |
| 165 | + box-shadow: 0 4rpx 12rpx rgba(240, 147, 251, 0.4); | |
| 166 | +} | |
| 167 | + | |
| 168 | +/* 清空按钮 */ | |
| 169 | +.btn-clear { | |
| 170 | + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); | |
| 171 | + color: #8b4513; | |
| 172 | + box-shadow: 0 8rpx 24rpx rgba(252, 182, 159, 0.3); | |
| 173 | +} | |
| 174 | + | |
| 175 | +.btn-clear:active { | |
| 176 | + transform: translateY(4rpx) scale(0.98); | |
| 177 | + box-shadow: 0 4rpx 12rpx rgba(252, 182, 159, 0.3); | |
| 178 | +} | |
| 179 | + | |
| 180 | +/* 语音类型选择区域 */ | |
| 181 | +.type-view { | |
| 182 | + width: 100%; | |
| 183 | + padding: 30rpx; | |
| 184 | + background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.9) 100%); | |
| 185 | + backdrop-filter: blur(20rpx); | |
| 186 | + border-radius: 24rpx; | |
| 187 | + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08); | |
| 188 | + box-sizing: border-box; | |
| 189 | +} | |
| 190 | + | |
| 191 | +.type-title { | |
| 192 | + font-size: 28rpx; | |
| 193 | + font-weight: 600; | |
| 194 | + color: #666; | |
| 195 | + margin-bottom: 20rpx; | |
| 196 | + text-align: center; | |
| 197 | +} | |
| 198 | + | |
| 199 | +.type-button { | |
| 200 | + width: 100%; | |
| 201 | + height: 80rpx; | |
| 202 | + margin-bottom: 16rpx; | |
| 203 | + display: flex; | |
| 204 | + align-items: center; | |
| 205 | + justify-content: center; | |
| 206 | + border-radius: 40rpx; | |
| 207 | + font-size: 28rpx; | |
| 208 | + font-weight: 500; | |
| 209 | + transition: all 0.3s ease; | |
| 210 | + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); | |
| 211 | + color: #666; | |
| 212 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06); | |
| 213 | +} | |
| 214 | + | |
| 215 | +.type-button:last-child { | |
| 216 | + margin-bottom: 0; | |
| 217 | +} | |
| 218 | + | |
| 219 | +.type-button.selected { | |
| 220 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 221 | + color: white; | |
| 222 | + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4); | |
| 223 | + transform: scale(1.02); | |
| 224 | +} | |
| 225 | + | |
| 226 | +.type-button:active { | |
| 227 | + transform: translateY(2rpx) scale(0.98); | |
| 228 | +} | |
| 229 | + | |
| 230 | +.type-button.selected:active { | |
| 231 | + transform: translateY(2rpx) scale(1.0); | |
| 232 | +} | ... | ... |