Commit a01fd7ab020d82d02bf84582b1365e5a227e1467

Authored by xwenliang
1 parent 98ac06d9

refactor: use new ui

@@ -70,29 +70,6 @@ App({ @@ -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 // TTS接口请求方法 73 // TTS接口请求方法
97 async requestTTS(text, voice) { 74 async requestTTS(text, voice) {
98 return new Promise((resolve, reject) => { 75 return new Promise((resolve, reject) => {
@@ -165,5 +142,34 @@ App({ @@ -165,5 +142,34 @@ App({
165 } 142 }
166 return list; 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 "pages": [ 2 "pages": [
3 "pages/index/index", 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 "window": { 7 "window": {
7 "backgroundTextStyle": "light", 8 "backgroundTextStyle": "light",
8 "navigationBarBackgroundColor": "#fff", 9 "navigationBarBackgroundColor": "#fff",
9 "navigationBarTitleText": "文字转语音", 10 "navigationBarTitleText": "文字转语音",
10 - "navigationBarTextStyle": "black", 11 + "navigationBarTextStyle": "white",
11 "navigationStyle": "custom" 12 "navigationStyle": "custom"
12 }, 13 },
13 "sitemapLocation": "sitemap.json" 14 "sitemapLocation": "sitemap.json"
14 -}  
15 \ No newline at end of file 15 \ No newline at end of file
  16 +}
app.wxss
@@ -48,13 +48,9 @@ page { @@ -48,13 +48,9 @@ page {
48 padding-bottom: env(safe-area-inset-bottom); 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 \ No newline at end of file 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
  1 +{
  2 + "component": true,
  3 + "usingComponents": {}
  4 +}
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 Page({ 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 wx.showShareMenu(); 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,41 +5,12 @@
5 <view class="ball ball-1"></view> 5 <view class="ball ball-1"></view>
6 <view class="ball ball-2"></view> 6 <view class="ball ball-2"></view>
7 </view> 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 </view> 15 </view>
35 </view> 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 \ No newline at end of file 17 \ No newline at end of file
pages/index/index.wxss
@@ -27,14 +27,14 @@ page { @@ -27,14 +27,14 @@ page {
27 width: 400rpx; 27 width: 400rpx;
28 height: 400rpx; 28 height: 400rpx;
29 background-color: #4472c4; 29 background-color: #4472c4;
30 - animation: move-1 5s infinite; 30 + animation: move-1 8s ease-in-out infinite;
31 } 31 }
32 32
33 .ball-2 { 33 .ball-2 {
34 width: 500rpx; 34 width: 500rpx;
35 height: 500rpx; 35 height: 500rpx;
36 background-color: #00b050; 36 background-color: #00b050;
37 - animation: move-2 5s infinite; 37 + animation: move-2 10s ease-in-out infinite;
38 } 38 }
39 39
40 @keyframes move-1 { 40 @keyframes move-1 {
@@ -42,13 +42,13 @@ page { @@ -42,13 +42,13 @@ page {
42 transform: translate(0, 0); 42 transform: translate(0, 0);
43 } 43 }
44 25% { 44 25% {
45 - transform: translate(200rpx, 150rpx); 45 + transform: translate(500rpx, 600rpx);
46 } 46 }
47 50% { 47 50% {
48 - transform: translate(0, 300rpx); 48 + transform: translate(-300rpx, 1000rpx);
49 } 49 }
50 75% { 50 75% {
51 - transform: translate(-100rpx, 150rpx); 51 + transform: translate(-400rpx, 400rpx);
52 } 52 }
53 100% { 53 100% {
54 transform: translate(0, 0); 54 transform: translate(0, 0);
@@ -57,234 +57,77 @@ page { @@ -57,234 +57,77 @@ page {
57 57
58 @keyframes move-2 { 58 @keyframes move-2 {
59 0% { 59 0% {
60 - transform: translate(0, 0); 60 + transform: translate(100rpx, 100rpx);
61 } 61 }
62 25% { 62 25% {
63 - transform: translate(-200rpx, -180rpx); 63 + transform: translate(-500rpx, -200rpx);
64 } 64 }
65 50% { 65 50% {
66 - transform: translate(0, -320rpx); 66 + transform: translate(400rpx, 800rpx);
67 } 67 }
68 75% { 68 75% {
69 - transform: translate(100rpx, -180rpx); 69 + transform: translate(600rpx, 200rpx);
70 } 70 }
71 100% { 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 .container-custom { 76 .container-custom {
91 position: relative; 77 position: relative;
92 width: 100%; 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 box-sizing: border-box; 83 box-sizing: border-box;
104 -}  
105 -.text-area{  
106 - width: 100%;  
107 - max-width: 100%;  
108 padding: 20rpx; 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 position: relative; 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 display: flex; 91 display: flex;
  92 + flex-direction: column;
226 align-items: center; 93 align-items: center;
227 justify-content: center; 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 color: white; 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 \ No newline at end of file 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 \ No newline at end of file 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 \ No newline at end of file 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
1 { 1 {
2 "usingComponents": { 2 "usingComponents": {
  3 + "custom-nav-bar": "../../components/custom-nav-bar/index",
3 "audio-overlay": "../../components/audio-overlay/audio-overlay" 4 "audio-overlay": "../../components/audio-overlay/audio-overlay"
4 } 5 }
5 } 6 }
6 \ No newline at end of file 7 \ No newline at end of file
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
  1 +{
  2 + "usingComponents": {
  3 + "custom-nav-bar": "../../components/custom-nav-bar/index",
  4 + "audio-overlay": "../../components/audio-overlay/audio-overlay"
  5 + }
  6 +}
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 +}