Commit a01fd7ab020d82d02bf84582b1365e5a227e1467

Authored by xwenliang
1 parent 98ac06d9

refactor: use new ui

... ... @@ -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
  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 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
1 1 {
2 2 "usingComponents": {
  3 + "custom-nav-bar": "../../components/custom-nav-bar/index",
3 4 "audio-overlay": "../../components/audio-overlay/audio-overlay"
4 5 }
5 6 }
6 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 +}
... ...