<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>
|