2025-03-21 18:08:47 +08:00
|
|
|
|
<template>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-dialog :visible.sync="localVisible" width="90%" @close="handleClose" :show-close="false" :append-to-body="true"
|
|
|
|
|
|
:close-on-click-modal="true">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<button class="custom-close-btn" @click="handleClose">
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="scroll-wrapper">
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
|
|
|
|
|
|
<el-table v-loading="loading" :data="filteredTtsModels" style="width: 100%;" class="data-table"
|
|
|
|
|
|
header-row-class-name="table-header" :fit="true" element-loading-text="拼命加载中"
|
|
|
|
|
|
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<el-table-column label="选择" width="50" align="center">
|
2025-03-21 18:08:47 +08:00
|
|
|
|
<template slot-scope="scope">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-table-column label="音色编码" align="center">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<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>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-table-column label="音色名称" align="center">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<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>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-table-column label="语言类型" align="center">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<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>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-table-column v-if="!showReferenceColumns" label="试听" align="center" class-name="audio-column">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<div class="custom-audio-container">
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-input v-if="scope.row.editing" v-model="scope.row.voiceDemo" placeholder="请输入MP3地址"
|
|
|
|
|
|
class="audio-input">
|
2025-04-11 17:44:05 +08:00
|
|
|
|
</el-input>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo" />
|
2025-04-08 16:24:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-table-column v-if="!showReferenceColumns" label="备注" align="center">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<template slot-scope="scope">
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-input v-if="scope.row.editing" type="textarea" :rows="1" autosize v-model="scope.row.remark"
|
|
|
|
|
|
placeholder="这里是备注" class="remark-input"></el-input>
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<span v-else>{{ scope.row.remark }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-table-column v-if="showReferenceColumns" label="克隆音频路径" align="center">
|
2025-06-09 19:18:58 +08:00
|
|
|
|
<template slot-scope="scope">
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-input v-if="scope.row.editing" v-model="scope.row.referenceAudio" placeholder="这里是克隆音频路径"></el-input>
|
2025-06-09 19:18:58 +08:00
|
|
|
|
<span v-else>{{ scope.row.referenceAudio }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-table-column v-if="showReferenceColumns" label="克隆音频文本" align="center">
|
2025-06-09 19:18:58 +08:00
|
|
|
|
<template slot-scope="scope">
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-input v-if="scope.row.editing" v-model="scope.row.referenceText" placeholder="这里是克隆音频对应文本"></el-input>
|
2025-06-09 19:18:58 +08:00
|
|
|
|
<span v-else>{{ scope.row.referenceText }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<el-table-column label="操作" align="center" width="150">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<template v-if="!scope.row.editing">
|
2025-04-11 17:44:05 +08:00
|
|
|
|
<el-button type="text" size="mini" @click="startEdit(scope.row)" class="edit-btn">
|
|
|
|
|
|
编辑
|
2025-04-08 16:24:18 +08:00
|
|
|
|
</el-button>
|
2025-04-11 17:44:05 +08:00
|
|
|
|
<el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn">
|
2025-04-08 16:24:18 +08:00
|
|
|
|
删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-button v-else type="success" size="mini" @click="saveEdit(scope.row)" class="save-Tts">保存
|
2025-04-08 16:24:18 +08:00
|
|
|
|
</el-button>
|
2025-03-21 18:08:47 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
2025-04-08 16:24:18 +08:00
|
|
|
|
</div>
|
2025-03-21 18:08:47 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
<!-- 自定义滚动条 -->
|
|
|
|
|
|
<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>
|
2025-03-21 18:08:47 +08:00
|
|
|
|
</div>
|
2025-04-08 16:24:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
|
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
|
|
|
|
|
{{ selectAll ? '取消全选' : '全选' }}
|
|
|
|
|
|
</el-button>
|
2025-04-11 17:44:05 +08:00
|
|
|
|
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
|
2025-04-09 17:52:41 +08:00
|
|
|
|
新增
|
|
|
|
|
|
</el-button>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
<el-button type="primary" size="mini" @click="deleteRow(filteredTtsModels.filter(row => row.selected))"
|
|
|
|
|
|
style="background: red;border:None">删除
|
2025-04-11 14:08:31 +08:00
|
|
|
|
</el-button>
|
2025-04-08 16:24:18 +08:00
|
|
|
|
</div>
|
2025-03-21 18:08:47 +08:00
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-04-09 11:00:10 +08:00
|
|
|
|
import Api from "@/apis/api";
|
2025-06-30 16:56:56 +08:00
|
|
|
|
import AudioPlayer from './AudioPlayer.vue';
|
2025-04-08 16:24:18 +08:00
|
|
|
|
|
2025-03-21 18:08:47 +08:00
|
|
|
|
export default {
|
2025-06-30 16:56:56 +08:00
|
|
|
|
components: { AudioPlayer },
|
2025-03-21 18:08:47 +08:00
|
|
|
|
props: {
|
|
|
|
|
|
visible: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
2025-04-09 11:00:10 +08:00
|
|
|
|
},
|
|
|
|
|
|
ttsModelId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
required: true
|
2025-06-30 16:56:56 +08:00
|
|
|
|
},
|
|
|
|
|
|
modelConfig: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
2025-03-21 18:08:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2025-04-02 15:38:04 +08:00
|
|
|
|
localVisible: this.visible,
|
2025-03-21 18:08:47 +08:00
|
|
|
|
searchQuery: '',
|
|
|
|
|
|
editDialogVisible: false,
|
|
|
|
|
|
editVoiceData: {},
|
2025-04-09 11:00:10 +08:00
|
|
|
|
ttsModels: [],
|
2025-03-21 18:08:47 +08:00
|
|
|
|
currentPage: 1,
|
2025-04-09 17:52:41 +08:00
|
|
|
|
pageSize: 10000,
|
2025-04-09 11:00:10 +08:00
|
|
|
|
total: 0,
|
2025-04-08 16:24:18 +08:00
|
|
|
|
isDragging: false,
|
|
|
|
|
|
startY: 0,
|
|
|
|
|
|
scrollTop: 0,
|
|
|
|
|
|
selectAll: false,
|
2025-04-09 11:00:10 +08:00
|
|
|
|
selectedRows: [],
|
|
|
|
|
|
loading: false,
|
2025-06-30 16:56:56 +08:00
|
|
|
|
showReferenceColumns: false, // 控制是否显示参考列
|
2025-03-21 18:08:47 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
2025-04-02 15:38:04 +08:00
|
|
|
|
watch: {
|
|
|
|
|
|
visible(newVal) {
|
|
|
|
|
|
this.localVisible = newVal;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
if (newVal) {
|
2025-04-09 11:00:10 +08:00
|
|
|
|
this.currentPage = 1;
|
2025-06-30 16:56:56 +08:00
|
|
|
|
this.updateShowReferenceColumns(); // 更新显示状态
|
2025-04-09 11:00:10 +08:00
|
|
|
|
this.loadData(); // 对话框显示时加载数据
|
2025-04-08 16:24:18 +08:00
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.updateScrollbar();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-06-30 16:56:56 +08:00
|
|
|
|
modelConfig: {
|
|
|
|
|
|
handler(newVal) {
|
|
|
|
|
|
this.updateShowReferenceColumns();
|
|
|
|
|
|
},
|
|
|
|
|
|
immediate: true
|
|
|
|
|
|
},
|
2025-04-08 16:24:18 +08:00
|
|
|
|
filteredTtsModels() {
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.updateScrollbar();
|
|
|
|
|
|
});
|
2025-04-02 15:38:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-03-21 18:08:47 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
filteredTtsModels() {
|
|
|
|
|
|
return this.ttsModels.filter(model =>
|
2025-06-30 16:56:56 +08:00
|
|
|
|
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
|
2025-03-21 18:08:47 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-04-08 16:24:18 +08:00
|
|
|
|
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);
|
|
|
|
|
|
},
|
2025-03-21 18:08:47 +08:00
|
|
|
|
methods: {
|
2025-06-30 16:56:56 +08:00
|
|
|
|
// 更新是否显示参考列
|
|
|
|
|
|
updateShowReferenceColumns() {
|
|
|
|
|
|
if (this.modelConfig && this.modelConfig.configJson) {
|
|
|
|
|
|
const providerType = this.modelConfig.configJson.type;
|
|
|
|
|
|
this.showReferenceColumns = ['fishspeech', 'gpt_sovits_v2', 'gpt_sovits_v3'].includes(providerType);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showReferenceColumns = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-04-09 11:00:10 +08:00
|
|
|
|
loadData() {
|
2025-04-21 15:23:27 +08:00
|
|
|
|
this.loading = true;
|
2025-04-09 11:00:10 +08:00
|
|
|
|
const params = {
|
|
|
|
|
|
ttsModelId: this.ttsModelId,
|
|
|
|
|
|
page: this.currentPage,
|
|
|
|
|
|
limit: this.pageSize,
|
|
|
|
|
|
name: this.searchQuery
|
|
|
|
|
|
};
|
|
|
|
|
|
Api.timbre.getVoiceList(params, (data) => {
|
|
|
|
|
|
if (data.code === 0) {
|
2025-04-09 17:52:41 +08:00
|
|
|
|
this.ttsModels = data.data.list
|
2025-04-21 15:23:27 +08:00
|
|
|
|
.map(item => ({
|
|
|
|
|
|
id: item.id || '',
|
|
|
|
|
|
voiceCode: item.ttsVoice || '',
|
|
|
|
|
|
voiceName: item.name || '未命名音色',
|
|
|
|
|
|
languageType: item.languages || '',
|
|
|
|
|
|
remark: item.remark || '',
|
2025-06-09 19:18:58 +08:00
|
|
|
|
referenceAudio: item.referenceAudio || '',
|
|
|
|
|
|
referenceText: item.referenceText || '',
|
2025-04-21 15:23:27 +08:00
|
|
|
|
voiceDemo: item.voiceDemo || '',
|
|
|
|
|
|
selected: false,
|
|
|
|
|
|
editing: false,
|
|
|
|
|
|
sort: Number(item.sort)
|
|
|
|
|
|
}))
|
|
|
|
|
|
.sort((a, b) => a.sort - b.sort);
|
2025-04-09 11:00:10 +08:00
|
|
|
|
this.total = data.total;
|
|
|
|
|
|
} else {
|
2025-04-09 17:52:41 +08:00
|
|
|
|
this.$message.error({
|
|
|
|
|
|
message: data.msg || '获取音色列表失败',
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
2025-04-09 11:00:10 +08:00
|
|
|
|
}
|
2025-04-21 15:23:27 +08:00
|
|
|
|
this.loading = false;
|
2025-04-09 11:00:10 +08:00
|
|
|
|
}, (err) => {
|
|
|
|
|
|
console.error('加载失败:', err);
|
2025-04-09 17:52:41 +08:00
|
|
|
|
this.$message.error({
|
|
|
|
|
|
message: '加载音色数据失败',
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
2025-04-21 15:23:27 +08:00
|
|
|
|
this.loading = false;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
});
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-09 11:00:10 +08:00
|
|
|
|
handleClose() {
|
|
|
|
|
|
// 重置状态
|
|
|
|
|
|
this.ttsModels = [];
|
|
|
|
|
|
this.currentPage = 1;
|
|
|
|
|
|
this.total = 0;
|
|
|
|
|
|
this.selectAll = false;
|
|
|
|
|
|
this.searchQuery = '';
|
2025-06-30 16:56:56 +08:00
|
|
|
|
this.showReferenceColumns = false;
|
2025-04-09 11:00:10 +08:00
|
|
|
|
|
|
|
|
|
|
this.localVisible = false;
|
|
|
|
|
|
this.$emit('update:visible', false);
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
updateScrollbar() {
|
|
|
|
|
|
const container = this.$refs.tableContainer;
|
|
|
|
|
|
const scrollbarThumb = this.$refs.scrollbarThumb;
|
|
|
|
|
|
const scrollbarTrack = this.$refs.scrollbarTrack;
|
|
|
|
|
|
|
|
|
|
|
|
if (!container || !scrollbarThumb || !scrollbarTrack) return;
|
|
|
|
|
|
|
2025-06-30 16:56:56 +08:00
|
|
|
|
const { scrollHeight, clientHeight } = container;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
const trackHeight = scrollbarTrack.clientHeight;
|
|
|
|
|
|
const thumbHeight = Math.max((clientHeight / scrollHeight) * trackHeight, 20);
|
|
|
|
|
|
|
|
|
|
|
|
scrollbarThumb.style.height = `${thumbHeight}px`;
|
|
|
|
|
|
this.updateThumbPosition();
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
updateThumbPosition() {
|
|
|
|
|
|
const container = this.$refs.tableContainer;
|
|
|
|
|
|
const scrollbarThumb = this.$refs.scrollbarThumb;
|
|
|
|
|
|
const scrollbarTrack = this.$refs.scrollbarTrack;
|
|
|
|
|
|
|
|
|
|
|
|
if (!container || !scrollbarThumb || !scrollbarTrack) return;
|
|
|
|
|
|
|
2025-06-30 16:56:56 +08:00
|
|
|
|
const { scrollHeight, clientHeight, scrollTop } = container;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
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`;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
handleScroll() {
|
2025-04-09 11:00:10 +08:00
|
|
|
|
const container = this.$refs.tableContainer;
|
|
|
|
|
|
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
|
|
|
|
|
|
if (this.currentPage * this.pageSize < this.total) {
|
|
|
|
|
|
this.currentPage++;
|
|
|
|
|
|
this.loadData();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-08 16:24:18 +08:00
|
|
|
|
this.updateThumbPosition();
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
startDrag(e) {
|
|
|
|
|
|
this.isDragging = true;
|
|
|
|
|
|
this.startY = e.clientY;
|
|
|
|
|
|
this.scrollTop = this.$refs.tableContainer.scrollTop;
|
|
|
|
|
|
e.preventDefault();
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
stopDrag() {
|
|
|
|
|
|
this.isDragging = false;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
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;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
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);
|
|
|
|
|
|
},
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
startEdit(row) {
|
|
|
|
|
|
row.editing = true;
|
2025-06-30 16:56:56 +08:00
|
|
|
|
this.$set(row, 'originalData', { ...row });
|
2025-04-08 16:24:18 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
saveEdit(row) {
|
2025-05-14 14:43:36 +08:00
|
|
|
|
if (!row.voiceCode || !row.voiceName || !row.languageType) {
|
|
|
|
|
|
this.$message.error({
|
|
|
|
|
|
message: '音色编码、音色名称和语言类型不能为空',
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-09 17:52:41 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
id: row.id,
|
|
|
|
|
|
voiceCode: row.voiceCode,
|
|
|
|
|
|
voiceName: row.voiceName,
|
|
|
|
|
|
languageType: row.languageType,
|
|
|
|
|
|
remark: row.remark,
|
|
|
|
|
|
ttsModelId: this.ttsModelId,
|
|
|
|
|
|
voiceDemo: row.voiceDemo || '',
|
|
|
|
|
|
sort: row.sort
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-30 16:56:56 +08:00
|
|
|
|
// 只有在显示参考列的情况下才添加参考字段
|
|
|
|
|
|
if (this.showReferenceColumns) {
|
|
|
|
|
|
params.referenceAudio = row.referenceAudio;
|
|
|
|
|
|
params.referenceText = row.referenceText;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-09 17:52:41 +08:00
|
|
|
|
let res;
|
|
|
|
|
|
if (row.id) {
|
|
|
|
|
|
// 已有ID,执行更新操作
|
|
|
|
|
|
Api.timbre.updateVoice(params, (response) => {
|
|
|
|
|
|
res = response;
|
|
|
|
|
|
this.handleResponse(res, row);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 没有ID,执行新增操作
|
|
|
|
|
|
Api.timbre.saveVoice(params, (response) => {
|
|
|
|
|
|
res = response;
|
|
|
|
|
|
this.handleResponse(res, row);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('操作失败:', error);
|
|
|
|
|
|
// 异常情况下也恢复原始数据
|
|
|
|
|
|
if (row.originalData) {
|
|
|
|
|
|
Object.assign(row, row.originalData);
|
|
|
|
|
|
row.editing = false;
|
|
|
|
|
|
delete row.originalData;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$message.error({
|
|
|
|
|
|
message: '操作失败,请重试',
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleResponse(res, row) {
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
this.$message.success({
|
|
|
|
|
|
message: row.id ? '修改成功' : '保存成功',
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
|
|
|
|
|
row.editing = false;
|
|
|
|
|
|
delete row.originalData;
|
|
|
|
|
|
this.loadData(); // 刷新数据
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 保存失败时恢复原始数据
|
|
|
|
|
|
if (row.originalData) {
|
|
|
|
|
|
Object.assign(row, row.originalData);
|
|
|
|
|
|
row.editing = false;
|
|
|
|
|
|
delete row.originalData;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$message.error({
|
|
|
|
|
|
message: res.msg || (row.id ? '修改失败' : '保存失败'),
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-04-08 16:24:18 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
toggleSelectAll() {
|
|
|
|
|
|
this.selectAll = !this.selectAll;
|
|
|
|
|
|
this.filteredTtsModels.forEach(row => {
|
|
|
|
|
|
row.selected = this.selectAll;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
addNew() {
|
2025-05-14 14:43:36 +08:00
|
|
|
|
const hasEditing = this.ttsModels.some(row => row.editing);
|
|
|
|
|
|
if (hasEditing) {
|
|
|
|
|
|
this.$message.warning('请先完成当前编辑再新增');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-09 17:52:41 +08:00
|
|
|
|
const maxSort = this.ttsModels.length > 0
|
2025-06-30 16:56:56 +08:00
|
|
|
|
? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0))
|
|
|
|
|
|
: 0;
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
const newRow = {
|
2025-04-09 17:52:41 +08:00
|
|
|
|
voiceCode: '',
|
|
|
|
|
|
voiceName: '',
|
2025-04-08 16:24:18 +08:00
|
|
|
|
languageType: '中文',
|
2025-04-09 17:52:41 +08:00
|
|
|
|
voiceDemo: '',
|
2025-04-08 16:24:18 +08:00
|
|
|
|
remark: '',
|
2025-06-09 19:18:58 +08:00
|
|
|
|
referenceAudio: '',
|
|
|
|
|
|
referenceText: '',
|
2025-04-08 16:24:18 +08:00
|
|
|
|
selected: false,
|
2025-04-09 17:52:41 +08:00
|
|
|
|
editing: true,
|
|
|
|
|
|
sort: maxSort + 1
|
2025-04-08 16:24:18 +08:00
|
|
|
|
};
|
2025-04-09 17:52:41 +08:00
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
this.ttsModels.unshift(newRow);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
deleteRow(row) {
|
2025-06-30 16:56:56 +08:00
|
|
|
|
// 处理单个音色或音色数组
|
|
|
|
|
|
const voices = Array.isArray(row) ? row : [row];
|
2025-04-11 14:08:31 +08:00
|
|
|
|
|
2025-06-30 16:56:56 +08:00
|
|
|
|
if (Array.isArray(row) && row.length === 0) {
|
|
|
|
|
|
this.$message.warning("请先选择需要删除的音色");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const voiceCount = voices.length;
|
|
|
|
|
|
this.$confirm(`确定要删除选中的${voiceCount}个音色吗?`, "警告", {
|
|
|
|
|
|
confirmButtonText: "确定",
|
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
|
type: "warning",
|
|
|
|
|
|
distinguishCancelAndClose: true
|
|
|
|
|
|
}).then(() => {
|
|
|
|
|
|
const ids = voices.map(voice => voice.id);
|
|
|
|
|
|
if (ids.some(id => !id)) {
|
|
|
|
|
|
this.$message.error("存在无效的音色ID");
|
|
|
|
|
|
return;
|
2025-04-11 14:08:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-30 16:56:56 +08:00
|
|
|
|
Api.timbre.deleteVoice(ids, ({ data }) => {
|
|
|
|
|
|
if (data.code === 0) {
|
|
|
|
|
|
this.$message.success({
|
|
|
|
|
|
message: `成功删除${voiceCount}个参数`,
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
|
|
|
|
|
this.loadData(); // 刷新参数列表
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error({
|
|
|
|
|
|
message: data.msg || '删除失败,请重试',
|
|
|
|
|
|
showClose: true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-04-09 17:52:41 +08:00
|
|
|
|
});
|
2025-04-11 14:08:31 +08:00
|
|
|
|
}).catch(action => {
|
2025-06-30 16:56:56 +08:00
|
|
|
|
if (action === 'cancel') {
|
|
|
|
|
|
this.$message({
|
|
|
|
|
|
type: 'info',
|
|
|
|
|
|
message: '已取消删除操作',
|
|
|
|
|
|
duration: 1000
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message({
|
|
|
|
|
|
type: 'info',
|
|
|
|
|
|
message: '操作已关闭',
|
|
|
|
|
|
duration: 1000
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-04-11 14:08:31 +08:00
|
|
|
|
});
|
2025-04-09 17:52:41 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isValidAudioUrl(url) {
|
|
|
|
|
|
return url && (url.endsWith('.mp3') || url.endsWith('.ogg') || url.endsWith('.wav'));
|
2025-03-21 18:08:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-05-08 17:57:11 +08:00
|
|
|
|
<style lang="scss" scoped>
|
2025-04-08 16:24:18 +08:00
|
|
|
|
::v-deep .el-dialog {
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
overflow: hidden;
|
2025-04-11 17:44:05 +08:00
|
|
|
|
top: 1vh !important;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
::v-deep .el-dialog__header {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
|
margin: 0 !important;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
/* 表格样式 */
|
|
|
|
|
|
::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;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
outline: none;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
.custom-close-btn:hover {
|
|
|
|
|
|
color: #409EFF;
|
2025-03-21 18:08:47 +08:00
|
|
|
|
border-color: #409EFF;
|
|
|
|
|
|
}
|
2025-04-08 16:24:18 +08:00
|
|
|
|
|
|
|
|
|
|
/* 备注文本 */
|
|
|
|
|
|
::v-deep .remark-input .el-textarea__inner {
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: 1px solid #e6e6e6;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
resize: none;
|
|
|
|
|
|
max-height: 40px !important;
|
|
|
|
|
|
line-height: 1.5;
|
2025-04-16 12:55:07 +08:00
|
|
|
|
background-color: transparent !important;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .remark-input .el-textarea__inner:focus {
|
2025-04-16 12:55:07 +08:00
|
|
|
|
border-color: #409EFF !important;
|
|
|
|
|
|
outline: none;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:55:07 +08:00
|
|
|
|
::v-deep .remark-input .el-textarea__inner::placeholder {
|
|
|
|
|
|
color: #c0c4cc !important;
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-08 16:24:18 +08:00
|
|
|
|
/* 滚动容器 */
|
|
|
|
|
|
.scroll-wrapper {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
max-height: 55vh;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-container {
|
|
|
|
|
|
flex: 1;
|
2025-04-15 12:20:33 +08:00
|
|
|
|
overflow: auto;
|
2025-04-11 17:44:05 +08:00
|
|
|
|
scrollbar-width: none;
|
|
|
|
|
|
padding-right: 15px;
|
|
|
|
|
|
width: calc(100% - 16px);
|
2025-04-08 16:24:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-container::-webkit-scrollbar {
|
2025-04-11 17:44:05 +08:00
|
|
|
|
display: none;
|
2025-04-08 16:24:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 自定义滚动条 */
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-11 17:44:05 +08:00
|
|
|
|
/* 音频播放器容器样式 */
|
|
|
|
|
|
.custom-audio-container {
|
2025-04-15 12:20:33 +08:00
|
|
|
|
width: 90%;
|
|
|
|
|
|
margin: 0 auto;
|
2025-04-11 17:44:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:55:07 +08:00
|
|
|
|
.action-buttons .el-button {
|
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-11 17:44:05 +08:00
|
|
|
|
.edit-btn,
|
|
|
|
|
|
.delete-btn,
|
|
|
|
|
|
.save-btn {
|
|
|
|
|
|
margin: 0 8px;
|
|
|
|
|
|
color: #7079aa !important;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
2025-04-08 16:24:18 +08:00
|
|
|
|
|
2025-04-11 17:44:05 +08:00
|
|
|
|
.edit-btn:hover,
|
|
|
|
|
|
.delete-btn:hover,
|
|
|
|
|
|
.save-btn:hover {
|
|
|
|
|
|
color: #5f70f3 !important;
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn {
|
|
|
|
|
|
color: #5cca8e !important;
|
|
|
|
|
|
}
|
2025-04-15 12:20:33 +08:00
|
|
|
|
|
|
|
|
|
|
/* 表格单元格自适应 */
|
|
|
|
|
|
::v-deep .el-table__body-wrapper {
|
|
|
|
|
|
overflow-x: hidden !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-table td {
|
|
|
|
|
|
white-space: pre-wrap !important;
|
|
|
|
|
|
word-break: break-all !important;
|
|
|
|
|
|
}
|
2025-06-30 16:56:56 +08:00
|
|
|
|
|
2025-04-15 12:20:33 +08:00
|
|
|
|
/* 按钮组定位调整 */
|
|
|
|
|
|
.action-buttons {
|
|
|
|
|
|
position: static;
|
|
|
|
|
|
padding: 15px 0;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 输入框自适应 */
|
|
|
|
|
|
::v-deep .el-input__inner,
|
|
|
|
|
|
::v-deep .el-textarea__inner {
|
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 音频输入框特殊处理 */
|
|
|
|
|
|
.audio-input ::v-deep .el-input__inner {
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 操作按钮弹性布局 */
|
|
|
|
|
|
::v-deep .el-table__row .el-button {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin: 2px !important;
|
|
|
|
|
|
}
|
2025-04-08 16:24:18 +08:00
|
|
|
|
</style>
|