mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
完成“音色管理”页面
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="audio-container">
|
||||
<audio
|
||||
ref="audioRef"
|
||||
:src="audioUrl"
|
||||
@timeupdate="updateProgress"
|
||||
@loadedmetadata="updateDuration"
|
||||
style="display: none"
|
||||
></audio>
|
||||
|
||||
<div class="custom-controls">
|
||||
<!-- 播放/暂停按钮 -->
|
||||
<button class="play-btn" @click="togglePlay">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill="#4362b3"
|
||||
:d="isPlaying
|
||||
? 'M6 3h3v14H6zm5 0h3v14h-3z'
|
||||
: 'M5 3l12 7-12 7z'"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 时间显示 -->
|
||||
<span class="time-display">{{ formattedCurrentTime }}/{{ formattedDuration }}</span>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" @click="handleProgressClick">
|
||||
<div
|
||||
class="progress-track"
|
||||
:style="{ width: progress + '%' }"
|
||||
></div>
|
||||
<div
|
||||
class="progress-thumb"
|
||||
:style="{ left: progress + '%' }"
|
||||
@mousedown="startDrag"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音量控制 -->
|
||||
<div class="volume-control" ref="volumeControl">
|
||||
<button
|
||||
@click="toggleMute"
|
||||
@mouseenter="handleVolumeMouseEnter"
|
||||
@mouseleave="startVolumeSliderHideTimer"
|
||||
ref="volumeButton"
|
||||
class="volume-button"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" :d="volumeIconPath"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-show="showVolumeSlider"
|
||||
class="volume-slider-container"
|
||||
@mouseenter="cancelVolumeSliderHideTimer"
|
||||
@mouseleave="startVolumeSliderHideTimer"
|
||||
ref="volumeSlider"
|
||||
>
|
||||
<div class="volume-slider-track">
|
||||
<input
|
||||
type="range"
|
||||
v-model="volume"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
class="volume-slider"
|
||||
@input="handleVolumeChange"
|
||||
orient="vertical"
|
||||
>
|
||||
<div class="volume-slider-thumb" :style="{ bottom: volume * 100 + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
audioUrl: String
|
||||
})
|
||||
|
||||
// 音频元素引用
|
||||
const audioRef = ref(null)
|
||||
const volumeButton = ref(null)
|
||||
const volumeSlider = ref(null)
|
||||
|
||||
// 播放状态
|
||||
const isPlaying = ref(false)
|
||||
const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
const progress = ref(0)
|
||||
|
||||
// 音量状态
|
||||
const volume = ref(0.7)
|
||||
const isMuted = ref(false)
|
||||
const showVolumeSlider = ref(false)
|
||||
let volumeSliderHideTimer = null
|
||||
|
||||
// 格式化时间为 MM:SS
|
||||
const formatTime = (seconds) => {
|
||||
const sec = Math.floor(seconds || 0)
|
||||
return `${Math.floor(sec / 60)}:${(sec % 60).toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const formattedCurrentTime = computed(() => formatTime(currentTime.value))
|
||||
const formattedDuration = computed(() => formatTime(duration.value))
|
||||
|
||||
// 根据音量状态返回对应的图标路径
|
||||
const volumeIconPath = computed(() => {
|
||||
if (isMuted.value || volume.value === 0) {
|
||||
return 'M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z'
|
||||
}
|
||||
return 'M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z'
|
||||
})
|
||||
|
||||
// 播放控制
|
||||
const togglePlay = () => {
|
||||
if (isPlaying.value) {
|
||||
audioRef.value.pause()
|
||||
} else {
|
||||
audioRef.value.play()
|
||||
}
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
|
||||
const updateDuration = () => {
|
||||
if (audioRef.value) {
|
||||
duration.value = audioRef.value.duration
|
||||
}
|
||||
}
|
||||
|
||||
const updateProgress = () => {
|
||||
if (audioRef.value?.duration) {
|
||||
currentTime.value = audioRef.value.currentTime
|
||||
progress.value = (currentTime.value / duration.value) * 100
|
||||
}
|
||||
}
|
||||
|
||||
const handleProgressClick = (e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const percentage = ((e.clientX - rect.left) / rect.width) * 100
|
||||
seekToPercentage(percentage)
|
||||
}
|
||||
|
||||
// 音量控制
|
||||
const toggleMute = () => {
|
||||
isMuted.value = !isMuted.value
|
||||
audioRef.value.muted = isMuted.value
|
||||
|
||||
// 如果取消静音且音量为0,则恢复默认音量
|
||||
if (!isMuted.value && volume.value === 0) {
|
||||
volume.value = 0.5
|
||||
audioRef.value.volume = 0.5
|
||||
}
|
||||
}
|
||||
|
||||
const handleVolumeChange = (e) => {
|
||||
const newVolume = parseFloat(e.target.value)
|
||||
audioRef.value.volume = newVolume
|
||||
isMuted.value = newVolume === 0
|
||||
}
|
||||
|
||||
// 进度条拖拽功能
|
||||
let isDragging = false
|
||||
|
||||
const startDrag = (e) => {
|
||||
isDragging = true
|
||||
document.addEventListener('mousemove', handleDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleDrag = (e) => {
|
||||
if (!isDragging) return
|
||||
|
||||
const rect = document.querySelector('.progress-bar').getBoundingClientRect()
|
||||
const percentage = ((e.clientX - rect.left) / rect.width) * 100
|
||||
seekToPercentage(percentage)
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging = false
|
||||
document.removeEventListener('mousemove', handleDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
const seekToPercentage = (percentage) => {
|
||||
const clampedPercentage = Math.min(Math.max(percentage, 0), 100)
|
||||
progress.value = clampedPercentage
|
||||
audioRef.value.currentTime = (clampedPercentage / 100) * duration.value
|
||||
}
|
||||
|
||||
// 音量滑块显示/隐藏控制
|
||||
const startVolumeSliderHideTimer = () => {
|
||||
volumeSliderHideTimer = setTimeout(() => {
|
||||
showVolumeSlider.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const cancelVolumeSliderHideTimer = () => {
|
||||
clearTimeout(volumeSliderHideTimer)
|
||||
}
|
||||
|
||||
// 音量滑块定位
|
||||
const updateSliderPosition = () => {
|
||||
nextTick(() => {
|
||||
if (!volumeButton.value || !volumeSlider.value) return
|
||||
|
||||
const buttonRect = volumeButton.value.getBoundingClientRect()
|
||||
const slider = volumeSlider.value
|
||||
|
||||
// 计算相对于视口的位置
|
||||
const left = buttonRect.left + window.scrollX
|
||||
const top = buttonRect.top + window.scrollY
|
||||
|
||||
// 定位到按钮正中间,垂直居中
|
||||
slider.style.left = `${left + 5}px`
|
||||
slider.style.top = `${top - 85}px`
|
||||
})
|
||||
}
|
||||
|
||||
const handleVolumeMouseEnter = () => {
|
||||
showVolumeSlider.value = true
|
||||
updateSliderPosition()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (audioRef.value) {
|
||||
audioRef.value.volume = volume.value
|
||||
}
|
||||
window.addEventListener('resize', updateSliderPosition)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateSliderPosition)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.audio-container {
|
||||
background: #eef0fd;
|
||||
padding: 8px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.custom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: 12px;
|
||||
color: #5f7ba7;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-container:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 2px;
|
||||
background: #bfcadb;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: #4167ed;
|
||||
}
|
||||
|
||||
.progress-thumb {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #4167ed;
|
||||
border: 2px solid #d6dcfc;
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.volume-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: #8f95cd;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
position: fixed;
|
||||
padding: 10px 4px;
|
||||
background: #eef0fd;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.volume-slider-track {
|
||||
position: relative;
|
||||
height: 55px;
|
||||
width: 2px;
|
||||
background: #bfcadb;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.volume-slider-thumb {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background: #4167ed;
|
||||
border: 2px solid #d6dcfc;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
position: absolute;
|
||||
left: -14px;
|
||||
width: 30px;
|
||||
height: 60px;
|
||||
-webkit-appearance: slider-vertical;
|
||||
writing-mode: bt-lr;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.volume-slider-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
height: calc(100% * v-bind(volume));
|
||||
background: #4167ed;
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +1,112 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="localVisible" width="80%" @close="handleClose">
|
||||
<el-row class="main-container">
|
||||
<el-col :span="4">
|
||||
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
|
||||
<el-menu-item index="EdgeTTS">EdgeTTS</el-menu-item>
|
||||
<el-menu-item index="DoubaoTTS">DoubaoTTS</el-menu-item>
|
||||
<el-menu-item index="TTS302AI">TTS302AI</el-menu-item>
|
||||
<el-menu-item index="CosyVoiceSiliconflow">CosyVoiceSiliconflow</el-menu-item>
|
||||
</el-menu>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="20">
|
||||
<div class="search-operate">
|
||||
<el-input placeholder="请输入音色名称查询" v-model="searchQuery" style="width: 300px; margin-right: 10px;"/>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button type="primary" plain @click="handleAddVoice">添加音色</el-button>
|
||||
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredTtsModels" style="width: 100%" border stripe header-row-class-name="table-header">
|
||||
<el-table-column label="音色编码" prop="voiceCode" width="150" align="center"></el-table-column>
|
||||
<el-table-column label="音色名称" prop="voiceName" width="180" align="center"></el-table-column>
|
||||
<el-table-column label="语言类型" prop="languageType" width="120" align="center"></el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<el-dialog
|
||||
:visible.sync="localVisible"
|
||||
width="75%"
|
||||
@close="handleClose"
|
||||
:show-close="false"
|
||||
:append-to-body="true">
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
<div class="scroll-wrapper">
|
||||
<div
|
||||
class="table-container"
|
||||
ref="tableContainer"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<el-table
|
||||
:data="filteredTtsModels"
|
||||
style="width: 100%;"
|
||||
class="data-table"
|
||||
header-row-class-name="table-header"
|
||||
:fit="true">
|
||||
<el-table-column label="选择" width="50" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editVoice(scope.row)" style="color: #409EFF; margin-right: 15px;">修改</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteVoice(scope.row)" style="color: #F56C6C;">删除</el-button>
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音色编码" align="center" min-width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceCode"></el-input>
|
||||
<span v-else>{{ scope.row.voiceCode }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音色名称" align="center" min-width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceName"></el-input>
|
||||
<span v-else>{{ scope.row.voiceName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="语言类型" align="center" min-width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.languageType"></el-input>
|
||||
<span v-else>{{ scope.row.languageType }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="试听" align="center" min-width="225" class-name="audio-column">
|
||||
<template slot-scope="scope">
|
||||
<div class="custom-audio-container">
|
||||
<AudioPlayer :audioUrl="getAudioUrl(scope.row.voiceCode)"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="scope.row.editing"
|
||||
type="textarea"
|
||||
:rows="1"
|
||||
autosize
|
||||
v-model="scope.row.remark"
|
||||
placeholder="这里是备注"
|
||||
class="remark-input"></el-input>
|
||||
<span v-else>{{ scope.row.remark }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="!scope.row.editing">
|
||||
<el-button type="primary" size="mini" @click="startEdit(scope.row)"
|
||||
style="background: #5cca8e;border:None">编辑
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="deleteRow(scope.row)" style="background: red;border:None">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
size="mini"
|
||||
@click="saveEdit(scope.row)"
|
||||
class="save-Tts">保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total" layout="prev, pager, next" prev-text="<" next-text=">"/>
|
||||
<!-- 自定义滚动条 -->
|
||||
<div class="custom-scrollbar" ref="scrollbar">
|
||||
<div class="custom-scrollbar-track" ref="scrollbarTrack" @click="handleTrackClick">
|
||||
<div class="custom-scrollbar-thumb" ref="scrollbarThumb" @mousedown="startDrag"></div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose" size="medium">关闭</el-button>
|
||||
<el-button type="primary" @click="handleImportExport" size="medium">导入导出配置</el-button>
|
||||
</div>
|
||||
<EditVoiceDialog :showDialog="editDialogVisible" :voiceData="editVoiceData" @update:showDialog="editDialogVisible = $event" @saveVoice="handleSaveEditedVoice"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
||||
{{ selectAll ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="addNew" style="background: #f6cf79;border: None; color: #000012">新增</el-button>
|
||||
<el-button type="primary" size="mini" @click="batchDelete" style="background: red;border:None">删除</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditVoiceDialog from "@/components/EditVoiceDialog.vue";
|
||||
import AudioPlayer from './AudioPlayer.vue'
|
||||
|
||||
export default {
|
||||
components: { EditVoiceDialog },
|
||||
components: {AudioPlayer},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
@@ -63,139 +121,357 @@ export default {
|
||||
editDialogVisible: false,
|
||||
editVoiceData: {},
|
||||
ttsModels: [
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: 'AAAA国少', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
{voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '', selected: false},
|
||||
],
|
||||
currentPage: 1,
|
||||
pageSize: 4,
|
||||
total: 20
|
||||
total: 20,
|
||||
isDragging: false,
|
||||
startY: 0,
|
||||
scrollTop: 0,
|
||||
selectAll: false,
|
||||
selectedRows: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
if (newVal) {
|
||||
this.$nextTick(() => {
|
||||
this.updateScrollbar();
|
||||
});
|
||||
}
|
||||
},
|
||||
filteredTtsModels() {
|
||||
this.$nextTick(() => {
|
||||
this.updateScrollbar();
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTtsModels() {
|
||||
return this.ttsModels.filter(model =>
|
||||
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateScrollbar();
|
||||
window.addEventListener('resize', this.updateScrollbar);
|
||||
window.addEventListener('mouseup', this.stopDrag);
|
||||
window.addEventListener('mousemove', this.handleDrag);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.updateScrollbar);
|
||||
window.removeEventListener('mouseup', this.stopDrag);
|
||||
window.removeEventListener('mousemove', this.handleDrag);
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.ttsModels.forEach(model => {
|
||||
model.remark = '';
|
||||
});
|
||||
},
|
||||
handleModelSelect(index) {
|
||||
this.activeModel = index;
|
||||
// 可根据选中模型加载对应数据
|
||||
getAudioUrl(voiceCode) {
|
||||
return `https://music.163.com/song/media/outer/url?id=5257138.mp3`;
|
||||
},
|
||||
handleSearch() {
|
||||
// 搜索
|
||||
updateScrollbar() {
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
|
||||
if (!container || !scrollbarThumb || !scrollbarTrack) return;
|
||||
|
||||
const {scrollHeight, clientHeight} = container;
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const thumbHeight = Math.max((clientHeight / scrollHeight) * trackHeight, 20);
|
||||
|
||||
scrollbarThumb.style.height = `${thumbHeight}px`;
|
||||
this.updateThumbPosition();
|
||||
},
|
||||
handleAddVoice() {
|
||||
// 添加音色
|
||||
updateThumbPosition() {
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
|
||||
if (!container || !scrollbarThumb || !scrollbarTrack) return;
|
||||
|
||||
const {scrollHeight, clientHeight, scrollTop} = container;
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const thumbHeight = scrollbarThumb.clientHeight;
|
||||
const maxTop = trackHeight - thumbHeight;
|
||||
const thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - thumbHeight);
|
||||
|
||||
scrollbarThumb.style.top = `${Math.min(thumbTop, maxTop)}px`;
|
||||
},
|
||||
handleBatchDelete() {
|
||||
// 批量删除
|
||||
handleScroll() {
|
||||
this.updateThumbPosition();
|
||||
},
|
||||
editVoice(voice) {
|
||||
this.editVoiceData = { ...voice };
|
||||
this.editDialogVisible = true;
|
||||
startDrag(e) {
|
||||
this.isDragging = true;
|
||||
this.startY = e.clientY;
|
||||
this.scrollTop = this.$refs.tableContainer.scrollTop;
|
||||
e.preventDefault();
|
||||
},
|
||||
handleSaveEditedVoice(voiceForm) {
|
||||
const index = this.ttsModels.findIndex(item => item.voiceCode === voiceForm.voiceCode);
|
||||
if (index !== -1) {
|
||||
this.ttsModels.splice(index, 1, voiceForm);
|
||||
}
|
||||
stopDrag() {
|
||||
this.isDragging = false;
|
||||
},
|
||||
deleteVoice(voice) {
|
||||
// 删除
|
||||
handleDrag(e) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
const deltaY = e.clientY - this.startY;
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const thumbHeight = scrollbarThumb.clientHeight;
|
||||
const maxScrollTop = container.scrollHeight - container.clientHeight;
|
||||
|
||||
const scrollRatio = (trackHeight - thumbHeight) / maxScrollTop;
|
||||
container.scrollTop = this.scrollTop + deltaY / scrollRatio;
|
||||
},
|
||||
handleImportExport() {
|
||||
// 导入导出
|
||||
handleTrackClick(e) {
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
|
||||
if (!container || !scrollbarTrack || !scrollbarThumb) return;
|
||||
|
||||
const trackRect = scrollbarTrack.getBoundingClientRect();
|
||||
const thumbHeight = scrollbarThumb.clientHeight;
|
||||
const clickPosition = e.clientY - trackRect.top;
|
||||
const thumbCenter = clickPosition - thumbHeight / 2;
|
||||
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const maxTop = trackHeight - thumbHeight;
|
||||
const newTop = Math.max(0, Math.min(thumbCenter, maxTop));
|
||||
|
||||
scrollbarThumb.style.top = `${newTop}px`;
|
||||
container.scrollTop = (newTop / (trackHeight - thumbHeight)) * (container.scrollHeight - container.clientHeight);
|
||||
},
|
||||
// 按钮组
|
||||
startEdit(row) {
|
||||
row.editing = true;
|
||||
this.$set(row, 'originalData', {...row});
|
||||
},
|
||||
|
||||
saveEdit(row) {
|
||||
row.editing = false;
|
||||
delete row.originalData;
|
||||
// 这里可以添加保存到服务器的逻辑
|
||||
},
|
||||
|
||||
toggleSelectAll() {
|
||||
this.selectAll = !this.selectAll;
|
||||
this.filteredTtsModels.forEach(row => {
|
||||
row.selected = this.selectAll;
|
||||
});
|
||||
},
|
||||
|
||||
addNew() {
|
||||
const newRow = {
|
||||
voiceCode: '新编码',
|
||||
voiceName: '新音色',
|
||||
languageType: '中文',
|
||||
remark: '',
|
||||
selected: false,
|
||||
editing: true
|
||||
};
|
||||
this.ttsModels.unshift(newRow);
|
||||
},
|
||||
|
||||
deleteRow(row) {
|
||||
const index = this.ttsModels.indexOf(row);
|
||||
this.ttsModels.splice(index, 1);
|
||||
},
|
||||
|
||||
batchDelete() {
|
||||
this.ttsModels = this.ttsModels.filter(row => !row.selected);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
padding: 20px;
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
top: 8vh !important;
|
||||
}
|
||||
|
||||
.model-menu {
|
||||
border-right: 1px solid #ebeef5;
|
||||
height: calc(100vh - 300px);
|
||||
background-color: #fafafa;
|
||||
::v-deep .el-dialog__header {
|
||||
display: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.search-operate {
|
||||
margin-bottom: 20px;
|
||||
/* 表格样式 */
|
||||
::v-deep .data-table .el-table__header th {
|
||||
color: black;
|
||||
padding: 6px 0 !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__row td {
|
||||
padding: 8px 0 12px !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table.el-table::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__header-wrapper {
|
||||
border-bottom: 2px solid #f1f2fb !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__body-wrapper .el-table__body td {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
padding: 20px 20px 0;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
::v-deep .table-header th {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
::v-deep .el-table--border td, ::v-deep .el-table--border th {
|
||||
border-right: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination {
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .btn-prev, ::v-deep .el-pagination .btn-next {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .btn-prev:hover, ::v-deep .el-pagination .btn-next:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li.active {
|
||||
background-color: #409EFF;
|
||||
color: #fff;
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 备注文本 */
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e6e6e6;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
max-height: 40px !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||
color: black !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f4f6fa;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner:focus {
|
||||
background-color: #edeffb;
|
||||
}
|
||||
|
||||
/* 滚动容器 */
|
||||
.scroll-wrapper {
|
||||
display: flex;
|
||||
max-height: 55vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
padding-right: 15px; /* 为滚动条留出空间 */
|
||||
width: calc(100% - 16px); /* 减去滚动条宽度 */
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.custom-scrollbar {
|
||||
width: 8px;
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
height: 100%;
|
||||
top: 55px;
|
||||
}
|
||||
|
||||
.custom-scrollbar-track {
|
||||
position: relative;
|
||||
height: 380px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-scrollbar-thumb {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: #9dade7;
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.custom-scrollbar-thumb:hover {
|
||||
background: #6b84d9;
|
||||
}
|
||||
|
||||
.custom-scrollbar-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.save-Tts {
|
||||
background: #796dea;
|
||||
border: None;
|
||||
}
|
||||
|
||||
.save-Tts:hover {
|
||||
background: #8b80f0;
|
||||
}
|
||||
|
||||
.custom-audio-container audio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 新增按钮组样式 */
|
||||
.action-buttons {
|
||||
bottom: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user