|
|
@@ -13,13 +13,15 @@
|
|
|
<view class="card input-card">
|
|
|
<view class="card-header">
|
|
|
<text class="card-title">输入文本</text>
|
|
|
- <text class="word-count">{{ text.length }} 字</text>
|
|
|
+ <text class="word-count" :class="{ warning: text.length > 1800, error: text.length >= 2000 }">
|
|
|
+ {{ text.length }}/2000 字
|
|
|
+ </text>
|
|
|
</view>
|
|
|
<textarea
|
|
|
v-model="text"
|
|
|
class="text-input"
|
|
|
placeholder="请输入要转换的文本内容..."
|
|
|
- :maxlength="50000"
|
|
|
+ :maxlength="2000"
|
|
|
auto-height
|
|
|
/>
|
|
|
<view class="input-actions">
|
|
|
@@ -37,10 +39,10 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 音色选择 -->
|
|
|
+ <!-- 音色选择 - 网格布局 -->
|
|
|
<view class="card voice-card">
|
|
|
<text class="card-title">选择音色</text>
|
|
|
- <scroll-view scroll-x class="voice-list">
|
|
|
+ <view class="voice-grid">
|
|
|
<view
|
|
|
v-for="voice in audioStore.voices"
|
|
|
:key="voice.id"
|
|
|
@@ -52,16 +54,18 @@
|
|
|
<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>
|
|
|
|
|
|
<!-- 参数调节 -->
|
|
|
<view class="card params-card">
|
|
|
<text class="card-title">参数调节</text>
|
|
|
<view class="param-item">
|
|
|
- <text class="param-label">语速:{{ voiceParams.speed.toFixed(1) }}x</text>
|
|
|
+ <view class="param-header">
|
|
|
+ <text class="param-label">语速</text>
|
|
|
+ <view class="param-value">{{ voiceParams.speed.toFixed(1) }}x</view>
|
|
|
+ </view>
|
|
|
<slider
|
|
|
:value="(voiceParams.speed - 0.5) * 100 / 1.5"
|
|
|
:min="0"
|
|
|
@@ -71,9 +75,16 @@
|
|
|
backgroundColor="#e5e7eb"
|
|
|
block-size="20"
|
|
|
/>
|
|
|
+ <view class="param-range">
|
|
|
+ <text>0.5x</text>
|
|
|
+ <text>2.0x</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
<view class="param-item">
|
|
|
- <text class="param-label">音调:{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</text>
|
|
|
+ <view class="param-header">
|
|
|
+ <text class="param-label">音调</text>
|
|
|
+ <view class="param-value">{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</view>
|
|
|
+ </view>
|
|
|
<slider
|
|
|
:value="(voiceParams.pitch + 500) / 10"
|
|
|
:min="0"
|
|
|
@@ -83,9 +94,16 @@
|
|
|
backgroundColor="#e5e7eb"
|
|
|
block-size="20"
|
|
|
/>
|
|
|
+ <view class="param-range">
|
|
|
+ <text>-500</text>
|
|
|
+ <text>+500</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
<view class="param-item">
|
|
|
- <text class="param-label">音量:{{ voiceParams.volume }}%</text>
|
|
|
+ <view class="param-header">
|
|
|
+ <text class="param-label">音量</text>
|
|
|
+ <view class="param-value">{{ voiceParams.volume }}%</view>
|
|
|
+ </view>
|
|
|
<slider
|
|
|
:value="voiceParams.volume"
|
|
|
:min="0"
|
|
|
@@ -95,6 +113,10 @@
|
|
|
backgroundColor="#e5e7eb"
|
|
|
block-size="20"
|
|
|
/>
|
|
|
+ <view class="param-range">
|
|
|
+ <text>0%</text>
|
|
|
+ <text>100%</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -103,8 +125,11 @@
|
|
|
class="generate-btn"
|
|
|
:disabled="!canGenerate"
|
|
|
@click="handleGenerate"
|
|
|
+ @touchstart="btnPressed = true"
|
|
|
+ @touchend="btnPressed = false"
|
|
|
+ :class="{ pressed: btnPressed }"
|
|
|
>
|
|
|
- <text class="btn-text">{{ generating ? '生成中...' : '生成音频' }}</text>
|
|
|
+ <text class="btn-text">{{ generating ? '生成中...' : '🎵 生成音频' }}</text>
|
|
|
</button>
|
|
|
</view>
|
|
|
|
|
|
@@ -185,6 +210,7 @@ const voiceParams = ref<VoiceParams>({
|
|
|
volume: 50,
|
|
|
});
|
|
|
const generating = ref(false);
|
|
|
+const btnPressed = ref(false);
|
|
|
|
|
|
// 模板相关状态
|
|
|
const showTemplatePanel = ref(false);
|
|
|
@@ -348,6 +374,15 @@ async function handleGenerate() {
|
|
|
color: #6b7280;
|
|
|
}
|
|
|
|
|
|
+.word-count.warning {
|
|
|
+ color: #f59e0b;
|
|
|
+}
|
|
|
+
|
|
|
+.word-count.error {
|
|
|
+ color: #ef4444;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
.text-input {
|
|
|
width: 100%;
|
|
|
min-height: 300rpx;
|
|
|
@@ -397,13 +432,19 @@ async function handleGenerate() {
|
|
|
margin-top: 24rpx;
|
|
|
}
|
|
|
|
|
|
+/* 音色网格布局 - 2列 */
|
|
|
+.voice-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 16rpx;
|
|
|
+ margin-top: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
.voice-item {
|
|
|
- display: inline-flex;
|
|
|
+ display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
- width: 160rpx;
|
|
|
padding: 24rpx 16rpx;
|
|
|
- margin-right: 16rpx;
|
|
|
border-radius: 16rpx;
|
|
|
background: #f9fafb;
|
|
|
border: 2rpx solid transparent;
|
|
|
@@ -415,11 +456,14 @@ async function handleGenerate() {
|
|
|
border-color: #4f46e5;
|
|
|
}
|
|
|
|
|
|
-.voice-item.active .voice-name,
|
|
|
-.voice-item.active .voice-desc {
|
|
|
+.voice-item.active .voice-name {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
+.voice-item:active {
|
|
|
+ transform: scale(0.95);
|
|
|
+}
|
|
|
+
|
|
|
.voice-icon {
|
|
|
font-size: 48rpx;
|
|
|
margin-bottom: 12rpx;
|
|
|
@@ -441,22 +485,49 @@ async function handleGenerate() {
|
|
|
margin-top: 24rpx;
|
|
|
}
|
|
|
|
|
|
+.param-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
.param-label {
|
|
|
font-size: 26rpx;
|
|
|
color: #6b7280;
|
|
|
- margin-bottom: 12rpx;
|
|
|
- display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.param-value {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #4F46E5;
|
|
|
+ background: #EEF2FF;
|
|
|
+ padding: 4rpx 16rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.param-range {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.param-range text {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #9ca3af;
|
|
|
}
|
|
|
|
|
|
.generate-btn {
|
|
|
width: 100%;
|
|
|
- height: 96rpx;
|
|
|
+ height: 100rpx;
|
|
|
background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
|
|
|
border-radius: 24rpx;
|
|
|
border: none;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
+ box-shadow: 0 8rpx 24rpx rgba(79, 70, 229, 0.4);
|
|
|
+ transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.generate-btn::after {
|
|
|
@@ -465,12 +536,19 @@ async function handleGenerate() {
|
|
|
|
|
|
.generate-btn[disabled] {
|
|
|
background: #e5e7eb;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+
|
|
|
+.generate-btn.pressed {
|
|
|
+ transform: scale(0.96);
|
|
|
+ box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.3);
|
|
|
}
|
|
|
|
|
|
.btn-text {
|
|
|
- font-size: 32rpx;
|
|
|
+ font-size: 34rpx;
|
|
|
font-weight: 600;
|
|
|
color: #ffffff;
|
|
|
+ letter-spacing: 2rpx;
|
|
|
}
|
|
|
|
|
|
.ai-btn {
|