|
|
- <template>
- <view class="simple-tts-container">
- <view class="header">
- <text class="title">简单TTS示例</text>
- <text class="subtitle">快速集成文字转语音功能</text>
- </view>
-
- <!-- 快速转换区域 -->
- <view class="quick-section">
- <view class="input-group">
- <textarea
- v-model="quickText"
- placeholder="输入要转换的文本..."
- class="quick-input"
- maxlength="200"
- />
- <view class="char-count">{{ quickText.length }}/200</view>
- </view>
-
- <view class="quick-buttons">
- <button
- @click="quickConvert"
- :disabled="!quickText.trim() || loading"
- class="btn-convert"
- >
- {{ loading ? '转换中...' : '一键转换' }}
- </button>
-
- <button
- @click="quickPlay"
- :disabled="!hasAudio || playing"
- class="btn-play"
- >
- {{ playing ? '播放中...' : '播放' }}
- </button>
- </view>
- </view>
-
- <!-- 高级设置区域 -->
- <view class="advanced-section" v-if="showAdvanced">
- <view class="section-title" @click="toggleAdvanced">
- <text>高级设置</text>
- <text class="toggle-icon">{{ showAdvanced ? '▼' : '▶' }}</text>
- </view>
-
- <view class="advanced-content">
- <!-- 音色选择 -->
- <view class="setting-item">
- <text class="setting-label">音色:</text>
- <picker
- @change="onVoiceChange"
- :value="voiceIndex"
- :range="voiceOptions"
- range-key="name"
- class="setting-picker"
- >
- <view class="picker-display">
- {{ voiceOptions[voiceIndex]?.name || '默认音色' }}
- </view>
- </picker>
- </view>
-
- <!-- 语速设置 -->
- <view class="setting-item">
- <text class="setting-label">语速:{{ speedValue }}</text>
- <slider
- v-model="speedValue"
- :min="-2"
- :max="6"
- :step="1"
- class="setting-slider"
- />
- </view>
-
- <!-- 音量设置 -->
- <view class="setting-item">
- <text class="setting-label">音量:{{ volumeValue }}</text>
- <slider
- v-model="volumeValue"
- :min="-10"
- :max="10"
- :step="1"
- class="setting-slider"
- />
- </view>
- </view>
- </view>
-
- <!-- 状态显示区域 -->
- <view class="status-section" v-if="statusInfo">
- <view class="status-item">
- <text class="status-label">状态:</text>
- <text class="status-value">{{ statusInfo.status }}</text>
- </view>
- <view class="status-item" v-if="statusInfo.size">
- <text class="status-label">大小:</text>
- <text class="status-value">{{ statusInfo.size }}</text>
- </view>
- <view class="status-item" v-if="statusInfo.time">
- <text class="status-label">耗时:</text>
- <text class="status-value">{{ statusInfo.time }}秒</text>
- </view>
- </view>
-
- <!-- 预设文本区域 -->
- <view class="preset-section">
- <view class="section-title">预设文本</view>
- <view class="preset-buttons">
- <button
- v-for="(preset, index) in presetTexts"
- :key="index"
- @click="usePreset(preset)"
- class="preset-btn"
- >
- {{ preset.name }}
- </button>
- </view>
- </view>
- </view>
- </template>
-
- <script>
- import ttsService from '@/utils/uniapp-tts-service.js';
-
- export default {
- data() {
- return {
- // 基础数据
- quickText: '',
- loading: false,
- playing: false,
- hasAudio: false,
-
- // 高级设置
- showAdvanced: false,
- voiceOptions: [],
- voiceIndex: 0,
- speedValue: 0,
- volumeValue: 0,
-
- // 状态信息
- statusInfo: null,
-
- // 预设文本
- presetTexts: [
- { name: '问候语', text: '你好,欢迎使用文字转语音功能!' },
- { name: '感谢语', text: '谢谢您的使用,祝您生活愉快!' },
- { name: '提醒语', text: '请注意,您有新的消息需要查看。' },
- { name: '测试语', text: '这是一个语音测试,请检查音质是否清晰。' }
- ]
- }
- },
-
- async onLoad() {
- await this.initTTS();
- },
-
- methods: {
- /**
- * 初始化TTS服务
- */
- async initTTS() {
- try {
- uni.showLoading({ title: '初始化中...' });
-
- const result = await ttsService.init();
- if (result.success) {
- this.voiceOptions = ttsService.getVoiceList();
- console.log('TTS服务初始化成功');
- } else {
- throw new Error(result.error);
- }
- } catch (error) {
- console.error('TTS初始化失败:', error);
- uni.showToast({
- title: '初始化失败',
- icon: 'error'
- });
- } finally {
- uni.hideLoading();
- }
- },
-
- /**
- * 快速转换
- */
- async quickConvert() {
- if (!this.quickText.trim()) {
- uni.showToast({
- title: '请输入文本',
- icon: 'error'
- });
- return;
- }
-
- this.loading = true;
- this.statusInfo = { status: '转换中...' };
-
- try {
- const params = {
- text: this.quickText.trim(),
- speed: this.speedValue,
- voiceType: this.voiceOptions[this.voiceIndex]?.id || 0,
- volume: this.volumeValue,
- codec: 'wav'
- };
-
- const result = await ttsService.textToVoice(params);
-
- if (result.success) {
- this.hasAudio = true;
- this.statusInfo = {
- status: '转换成功',
- size: result.audioSize,
- time: result.convertTime
- };
-
- uni.showToast({
- title: '转换成功',
- icon: 'success'
- });
- }
- } catch (error) {
- console.error('转换失败:', error);
- this.statusInfo = { status: '转换失败' };
- uni.showToast({
- title: error.message || '转换失败',
- icon: 'error'
- });
- } finally {
- this.loading = false;
- }
- },
-
- /**
- * 快速播放
- */
- async quickPlay() {
- if (!this.hasAudio) {
- uni.showToast({
- title: '请先转换文本',
- icon: 'error'
- });
- return;
- }
-
- this.playing = true;
-
- try {
- await ttsService.playAudio();
-
- // 监听播放结束
- setTimeout(() => {
- this.playing = false;
- }, 100);
-
- } catch (error) {
- console.error('播放失败:', error);
- this.playing = false;
- uni.showToast({
- title: '播放失败',
- icon: 'error'
- });
- }
- },
-
- /**
- * 切换高级设置显示
- */
- toggleAdvanced() {
- this.showAdvanced = !this.showAdvanced;
- },
-
- /**
- * 音色选择变化
- */
- onVoiceChange(e) {
- this.voiceIndex = e.detail.value;
- },
-
- /**
- * 使用预设文本
- */
- usePreset(preset) {
- this.quickText = preset.text;
- uni.showToast({
- title: `已选择:${preset.name}`,
- icon: 'success'
- });
- },
-
- /**
- * 清空文本
- */
- clearText() {
- this.quickText = '';
- this.hasAudio = false;
- this.statusInfo = null;
- },
-
- /**
- * 停止播放
- */
- stopPlay() {
- ttsService.stopAudio();
- this.playing = false;
- }
- },
-
- onUnload() {
- // 页面卸载时清理资源
- ttsService.destroy();
- }
- }
- </script>
-
- <style scoped>
- .simple-tts-container {
- padding: 30rpx;
- background-color: #f8f9fa;
- min-height: 100vh;
- }
-
- .header {
- text-align: center;
- margin-bottom: 40rpx;
- }
-
- .title {
- display: block;
- font-size: 40rpx;
- font-weight: bold;
- color: #333;
- margin-bottom: 10rpx;
- }
-
- .subtitle {
- font-size: 28rpx;
- color: #666;
- }
-
- .quick-section {
- background-color: #fff;
- border-radius: 20rpx;
- padding: 30rpx;
- margin-bottom: 30rpx;
- box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
- }
-
- .input-group {
- position: relative;
- margin-bottom: 30rpx;
- }
-
- .quick-input {
- width: 100%;
- min-height: 160rpx;
- padding: 20rpx;
- border: 2rpx solid #e9ecef;
- border-radius: 12rpx;
- font-size: 30rpx;
- background-color: #fafbfc;
- resize: none;
- }
-
- .char-count {
- position: absolute;
- bottom: 10rpx;
- right: 15rpx;
- font-size: 24rpx;
- color: #999;
- }
-
- .quick-buttons {
- display: flex;
- gap: 20rpx;
- }
-
- .btn-convert, .btn-play {
- flex: 1;
- padding: 24rpx;
- border-radius: 12rpx;
- font-size: 32rpx;
- font-weight: 500;
- border: none;
- }
-
- .btn-convert {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #fff;
- }
-
- .btn-convert:disabled {
- background: #ccc;
- }
-
- .btn-play {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- color: #fff;
- }
-
- .btn-play:disabled {
- background: #ccc;
- }
-
- .advanced-section {
- background-color: #fff;
- border-radius: 20rpx;
- margin-bottom: 30rpx;
- overflow: hidden;
- box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
- }
-
- .section-title {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 30rpx;
- font-size: 32rpx;
- font-weight: 500;
- color: #333;
- border-bottom: 1rpx solid #f0f0f0;
- cursor: pointer;
- }
-
- .toggle-icon {
- font-size: 24rpx;
- color: #999;
- }
-
- .advanced-content {
- padding: 30rpx;
- }
-
- .setting-item {
- display: flex;
- align-items: center;
- margin-bottom: 30rpx;
- }
-
- .setting-item:last-child {
- margin-bottom: 0;
- }
-
- .setting-label {
- width: 120rpx;
- font-size: 28rpx;
- color: #333;
- }
-
- .setting-picker {
- flex: 1;
- }
-
- .picker-display {
- padding: 20rpx;
- border: 2rpx solid #e9ecef;
- border-radius: 8rpx;
- background-color: #fafbfc;
- font-size: 28rpx;
- }
-
- .setting-slider {
- flex: 1;
- margin-left: 20rpx;
- }
-
- .status-section {
- background-color: #fff;
- border-radius: 20rpx;
- padding: 30rpx;
- margin-bottom: 30rpx;
- box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
- }
-
- .status-item {
- display: flex;
- justify-content: space-between;
- margin-bottom: 15rpx;
- }
-
- .status-item:last-child {
- margin-bottom: 0;
- }
-
- .status-label {
- font-size: 28rpx;
- color: #666;
- }
-
- .status-value {
- font-size: 28rpx;
- color: #333;
- font-weight: 500;
- }
-
- .preset-section {
- background-color: #fff;
- border-radius: 20rpx;
- padding: 30rpx;
- box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
- }
-
- .preset-buttons {
- display: flex;
- flex-wrap: wrap;
- gap: 15rpx;
- }
-
- .preset-btn {
- padding: 15rpx 25rpx;
- border-radius: 25rpx;
- font-size: 26rpx;
- background-color: #f8f9fa;
- color: #495057;
- border: 2rpx solid #e9ecef;
- }
-
- .preset-btn:active {
- background-color: #e9ecef;
- }
- </style>
|