| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- <template>
- <view class="page">
- <!-- 顶部导航栏 -->
- <view class="nav-bar">
- <view class="nav-content">
- <text class="page-title">生成音频</text>
- </view>
- </view>
- <!-- 主内容区 -->
- <view class="main-content">
- <!-- 文本输入区 -->
- <view class="card input-card">
- <view class="card-header">
- <text class="card-title">输入文本</text>
- <text class="word-count">{{ text.length }} 字</text>
- </view>
- <textarea
- v-model="text"
- class="text-input"
- placeholder="请输入要转换的文本内容..."
- :maxlength="50000"
- auto-height
- />
- <view class="input-actions">
- <button class="action-btn ai-btn" @click="goToAIPage">
- <text>🤖 AI 生成</text>
- </button>
- <button class="action-btn" @click="clearText">清空</button>
- <button class="action-btn" @click="pasteText">粘贴</button>
- </view>
- <view class="input-hint">
- <text>支持长按粘贴文本内容</text>
- </view>
- </view>
- <!-- 音色选择 -->
- <view class="card voice-card">
- <text class="card-title">选择音色</text>
- <scroll-view scroll-x class="voice-list">
- <view
- v-for="voice in audioStore.voices"
- :key="voice.id"
- class="voice-item"
- :class="{ active: selectedVoice === voice.id }"
- @click="selectedVoice = voice.id"
- >
- <view class="voice-icon">
- <text>{{ voice.gender === 'female' ? '👩' : '👨' }}</text>
- </view>
- <text class="voice-name">{{ voice.name }}</text>
- <text class="voice-desc">{{ voice.description }}</text>
- </view>
- </scroll-view>
- </view>
- <!-- 参数调节 -->
- <view class="card params-card">
- <text class="card-title">参数调节</text>
- <view class="param-item">
- <text class="param-label">语速:{{ voiceParams.speed.toFixed(1) }}x</text>
- <slider
- :value="(voiceParams.speed - 0.5) * 100 / 1.5"
- :min="0"
- :max="100"
- @change="(e: any) => voiceParams.speed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
- activeColor="#4F46E5"
- backgroundColor="#e5e7eb"
- block-size="20"
- />
- </view>
- <view class="param-item">
- <text class="param-label">音调:{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</text>
- <slider
- :value="(voiceParams.pitch + 500) / 10"
- :min="0"
- :max="100"
- @change="(e: any) => voiceParams.pitch = Math.round(e.detail.value * 10 - 500)"
- activeColor="#4F46E5"
- backgroundColor="#e5e7eb"
- block-size="20"
- />
- </view>
- <view class="param-item">
- <text class="param-label">音量:{{ voiceParams.volume }}%</text>
- <slider
- :value="voiceParams.volume"
- :min="0"
- :max="100"
- @change="(e: any) => voiceParams.volume = e.detail.value"
- activeColor="#4F46E5"
- backgroundColor="#e5e7eb"
- block-size="20"
- />
- </view>
- </view>
- <!-- 生成按钮 -->
- <button
- class="generate-btn"
- :disabled="!canGenerate"
- @click="handleGenerate"
- >
- <text class="btn-text">{{ generating ? '生成中...' : '生成音频' }}</text>
- </button>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted } from 'vue';
- import { onShow } from '@dcloudio/uni-app';
- import { useUserStore } from '../../store/user';
- import { useAudioStore } from '../../store/audio';
- import type { VoiceParams } from '../../types';
- const userStore = useUserStore();
- const audioStore = useAudioStore();
- // 状态
- const text = ref('');
- const selectedVoice = ref('cherry');
- const voiceParams = ref<VoiceParams>({
- speed: 1.0,
- pitch: 0,
- volume: 50,
- });
- const generating = ref(false);
- // 计算属性
- const canGenerate = computed(() => {
- return text.value.trim().length > 0 && !generating.value;
- });
- // 初始化
- onMounted(async () => {
- await audioStore.fetchVoices();
- });
- // 页面显示时检查是否有 AI 生成的文本
- onShow(() => {
- const aiText = uni.getStorageSync('ai_generated_text');
- if (aiText) {
- text.value = aiText;
- uni.removeStorageSync('ai_generated_text');
- uni.showToast({ title: '已填充 AI 生成的文本', icon: 'success' });
- }
- });
- // 清空文本
- function clearText() {
- text.value = '';
- }
- // 粘贴文本
- async function pasteText() {
- const content = await uni.getClipboardData();
- if (content.data) {
- text.value = content.data;
- }
- }
- // 跳转到 AI 生成页面
- function goToAIPage() {
- uni.navigateTo({ url: '/pages/ai/index' });
- }
- // 生成音频
- async function handleGenerate() {
- if (!canGenerate.value) return;
- generating.value = true;
- try {
- const result = await audioStore.generateAudio(
- text.value,
- selectedVoice.value,
- voiceParams.value
- );
- uni.showToast({ title: '生成成功', icon: 'success' });
- // 清空文本,方便下次输入
- text.value = '';
- // 跳转到播放器
- uni.navigateTo({
- url: `/pages/player/index?id=${result.audioId}`,
- });
- } catch (error: any) {
- console.error('生成失败:', error);
- uni.showToast({
- title: error.message || '生成失败,请重试',
- icon: 'none'
- });
- } finally {
- generating.value = false;
- }
- }
- </script>
- <style scoped>
- .page {
- min-height: 100vh;
- background: #f5f5f5;
- }
- .nav-bar {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- height: 88rpx;
- background: #ffffff;
- z-index: 100;
- padding-top: 44rpx;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
- }
- .nav-content {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 32rpx;
- height: 88rpx;
- }
- .page-title {
- font-size: 34rpx;
- font-weight: 600;
- color: #1f2937;
- }
- .main-content {
- padding: 132rpx 32rpx 180rpx;
- }
- .card {
- background: #ffffff;
- border-radius: 24rpx;
- padding: 32rpx;
- margin-bottom: 24rpx;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
- }
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
- }
- .card-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #1f2937;
- }
- .word-count {
- font-size: 24rpx;
- color: #6b7280;
- }
- .text-input {
- width: 100%;
- min-height: 300rpx;
- font-size: 28rpx;
- color: #1f2937;
- line-height: 1.6;
- }
- .input-actions {
- display: flex;
- justify-content: flex-end;
- gap: 16rpx;
- margin-top: 16rpx;
- }
- .input-hint {
- margin-top: 12rpx;
- text-align: right;
- }
- .input-hint text {
- font-size: 22rpx;
- color: #9ca3af;
- }
- .action-btn {
- font-size: 24rpx;
- color: #6b7280;
- background: #f3f4f6;
- padding: 12rpx 24rpx;
- border-radius: 12rpx;
- border: none;
- transition: all 0.2s;
- }
- .action-btn:active {
- opacity: 0.7;
- transform: scale(0.98);
- }
- .action-btn::after {
- border: none;
- }
- .voice-list {
- white-space: nowrap;
- margin-top: 24rpx;
- }
- .voice-item {
- display: inline-flex;
- flex-direction: column;
- align-items: center;
- width: 160rpx;
- padding: 24rpx 16rpx;
- margin-right: 16rpx;
- border-radius: 16rpx;
- background: #f9fafb;
- border: 2rpx solid transparent;
- transition: all 0.3s;
- }
- .voice-item.active {
- background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
- border-color: #4f46e5;
- }
- .voice-item.active .voice-name,
- .voice-item.active .voice-desc {
- color: #ffffff;
- }
- .voice-icon {
- font-size: 48rpx;
- margin-bottom: 12rpx;
- }
- .voice-name {
- font-size: 26rpx;
- font-weight: 500;
- color: #1f2937;
- margin-bottom: 4rpx;
- }
- .voice-desc {
- font-size: 20rpx;
- color: #6b7280;
- }
- .param-item {
- margin-top: 24rpx;
- }
- .param-label {
- font-size: 26rpx;
- color: #6b7280;
- margin-bottom: 12rpx;
- display: block;
- }
- .generate-btn {
- width: 100%;
- height: 96rpx;
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
- border-radius: 24rpx;
- border: none;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .generate-btn::after {
- border: none;
- }
- .generate-btn[disabled] {
- background: #e5e7eb;
- }
- .btn-text {
- font-size: 32rpx;
- font-weight: 600;
- color: #ffffff;
- }
- .ai-btn {
- display: flex;
- align-items: center;
- gap: 8rpx;
- }
- </style>
|