Merge pull request #723 from xinnan-tech/web_Timbre_api

Web timbre api
This commit is contained in:
CGD
2025-04-09 17:54:01 +08:00
committed by GitHub
5 changed files with 379 additions and 49 deletions
+4 -1
View File
@@ -4,13 +4,15 @@ import agent from './module/agent.js'
import device from './module/device.js' import device from './module/device.js'
import model from './module/model.js' import model from './module/model.js'
import user from './module/user.js' import user from './module/user.js'
import timbre from "./module/timbre.js";
/** /**
* 接口地址 * 接口地址
* 开发时自动读取使用.env.development文件 * 开发时自动读取使用.env.development文件
* 编译时自动读取使用.env.production文件 * 编译时自动读取使用.env.production文件
*/ */
const DEV_API_SERVICE = process.env.VUE_APP_API_BASE_URL // const DEV_API_SERVICE = process.env.VUE_APP_API_BASE_URL
const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi'
/** /**
* 根据开发环境返回接口url * 根据开发环境返回接口url
@@ -29,4 +31,5 @@ export default {
agent, agent,
device, device,
model, model,
timbre,
} }
+102
View File
@@ -0,0 +1,102 @@
// timbre.js
import {getServiceUrl} from '../api';
import RequestService from '../httpRequest';
export default {
/**
* 获取音色数据
*/
getVoiceList(params, callback) {
const queryParams = new URLSearchParams({
ttsModelId: params.ttsModelId,
page: params.page || 1,
limit: params.limit || 10,
name: params.name || ''
}).toString();
RequestService.sendRequest()
.url(`${getServiceUrl()}/ttsVoice?${queryParams}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res.data || []);
})
.fail((err) => {
console.error('获取音色列表失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceList(params, callback);
});
}).send();
},
/**
* 音色保存
*/
saveVoice(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/ttsVoice`)
.method('POST')
.data(JSON.stringify({
languages: params.languageType,
name: params.voiceName,
remark: params.remark,
sort: params.sort,
ttsModelId: params.ttsModelId,
ttsVoice: params.voiceCode,
voiceDemo: params.voiceDemo || ''
}))
.success((res) => {
callback(res.data);
})
.fail((err) => {
console.error('保存音色失败:', err);
RequestService.reAjaxFun(() => {
this.saveVoice(params, callback);
});
}).send();
},
/**
* 音色删除
*/
deleteVoice(id, callback) {
console.log('尝试删除音色,ID:', id);
RequestService.sendRequest()
.url(`${getServiceUrl()}/ttsVoice`)
.method('DELETE')
.data({ ids: [id] })
.success((res) => {
RequestService.clearRequestTime()
callback(res);
})
.fail((err) => {
console.error('删除音色失败:', err);
RequestService.reAjaxFun(() => {
this.deleteVoice(id, callback);
});
}).send();
},
/**
* 音色修改
*/
updateVoice(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/ttsVoice/${params.id}`)
.method('PUT')
.data(JSON.stringify({
languages: params.languageType,
name: params.voiceName,
remark: params.remark,
ttsModelId: params.ttsModelId,
ttsVoice: params.voiceCode,
voiceDemo: params.voiceDemo || ''
}))
.success((res) => {
callback(res.data);
})
.fail((err) => {
console.error('修改音色失败:', err);
RequestService.reAjaxFun(() => {
this.updateVoice(params, callback);
});
}).send();
}
}
@@ -360,8 +360,8 @@ onUnmounted(() => {
left: -14px; left: -14px;
width: 30px; width: 30px;
height: 60px; height: 60px;
-webkit-appearance: slider-vertical; writing-mode: vertical-lr;
writing-mode: bt-lr; direction: rtl;
opacity: 0; opacity: 0;
cursor: pointer; cursor: pointer;
z-index: 2; z-index: 2;
+260 -44
View File
@@ -4,7 +4,8 @@
width="75%" width="75%"
@close="handleClose" @close="handleClose"
:show-close="false" :show-close="false"
:append-to-body="true"> :append-to-body="true"
:close-on-click-modal="true">
<button class="custom-close-btn" @click="handleClose"> <button class="custom-close-btn" @click="handleClose">
× ×
</button> </button>
@@ -15,6 +16,7 @@
@scroll="handleScroll" @scroll="handleScroll"
> >
<el-table <el-table
v-loading="loading"
:data="filteredTtsModels" :data="filteredTtsModels"
style="width: 100%;" style="width: 100%;"
class="data-table" class="data-table"
@@ -46,7 +48,14 @@
<el-table-column label="试听" align="center" min-width="225" class-name="audio-column"> <el-table-column label="试听" align="center" min-width="225" class-name="audio-column">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="custom-audio-container"> <div class="custom-audio-container">
<AudioPlayer :audioUrl="getAudioUrl(scope.row.voiceCode)"/> <el-input
v-if="scope.row.editing"
v-model="scope.row.voiceDemo"
placeholder="请输入MP3地址"
size="mini"
class="audio-input"
></el-input>
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo"/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -96,7 +105,9 @@
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None"> <el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
{{ selectAll ? '取消全选' : '全选' }} {{ selectAll ? '取消全选' : '全选' }}
</el-button> </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="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> <el-button type="primary" size="mini" @click="batchDelete" style="background: red;border:None">删除</el-button>
</div> </div>
</el-dialog> </el-dialog>
@@ -104,6 +115,7 @@
<script> <script>
import AudioPlayer from './AudioPlayer.vue' import AudioPlayer from './AudioPlayer.vue'
import Api from "@/apis/api";
export default { export default {
components: {AudioPlayer}, components: {AudioPlayer},
@@ -111,49 +123,36 @@ export default {
visible: { visible: {
type: Boolean, type: Boolean,
default: false default: false
},
ttsModelId: {
type: String,
required: true
} }
}, },
data() { data() {
return { return {
localVisible: this.visible, localVisible: this.visible,
activeModel: 'EdgeTTS',
searchQuery: '', searchQuery: '',
editDialogVisible: false, editDialogVisible: false,
editVoiceData: {}, editVoiceData: {},
ttsModels: [ ttsModels: [],
{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, currentPage: 1,
pageSize: 4, pageSize: 10000,
total: 20, total: 0,
isDragging: false, isDragging: false,
startY: 0, startY: 0,
scrollTop: 0, scrollTop: 0,
selectAll: false, selectAll: false,
selectedRows: [] selectedRows: [],
loading: false,
}; };
}, },
watch: { watch: {
visible(newVal) { visible(newVal) {
this.localVisible = newVal; this.localVisible = newVal;
if (newVal) { if (newVal) {
this.currentPage = 1;
this.loadData(); // 对话框显示时加载数据
this.$nextTick(() => { this.$nextTick(() => {
this.updateScrollbar(); this.updateScrollbar();
}); });
@@ -184,16 +183,57 @@ export default {
window.removeEventListener('mousemove', this.handleDrag); window.removeEventListener('mousemove', this.handleDrag);
}, },
methods: { methods: {
handleClose() { loadData() {
this.localVisible = false; const params = {
this.$emit('update:visible', false); ttsModelId: this.ttsModelId,
this.ttsModels.forEach(model => { page: this.currentPage,
model.remark = ''; limit: this.pageSize,
name: this.searchQuery
};
Api.timbre.getVoiceList(params, (data) => {
if (data.code === 0) {
this.ttsModels = data.data.list
.map(item => ({
id: item.id || '',
voiceCode: item.ttsVoice || '',
voiceName: item.name || '未命名音色',
languageType: item.languages || '',
remark: item.remark || '',
voiceDemo: item.voiceDemo || '',
selected: false,
editing: false,
sort: Number(item.sort)
}))
.sort((a, b) => a.sort - b.sort);
this.total = data.total;
} else {
this.$message.error({
message: data.msg || '获取音色列表失败',
showClose: true
});
}
}, (err) => {
console.error('加载失败:', err);
this.$message.error({
message: '加载音色数据失败',
showClose: true
});
}); });
}, },
getAudioUrl(voiceCode) {
return `https://music.163.com/song/media/outer/url?id=5257138.mp3`; handleClose() {
// 重置状态
this.ttsModels = [];
this.currentPage = 1;
this.total = 0;
this.selectAll = false;
this.searchQuery = '';
this.localVisible = false;
this.$emit('update:visible', false);
}, },
updateScrollbar() { updateScrollbar() {
const container = this.$refs.tableContainer; const container = this.$refs.tableContainer;
const scrollbarThumb = this.$refs.scrollbarThumb; const scrollbarThumb = this.$refs.scrollbarThumb;
@@ -208,6 +248,7 @@ export default {
scrollbarThumb.style.height = `${thumbHeight}px`; scrollbarThumb.style.height = `${thumbHeight}px`;
this.updateThumbPosition(); this.updateThumbPosition();
}, },
updateThumbPosition() { updateThumbPosition() {
const container = this.$refs.tableContainer; const container = this.$refs.tableContainer;
const scrollbarThumb = this.$refs.scrollbarThumb; const scrollbarThumb = this.$refs.scrollbarThumb;
@@ -223,18 +264,29 @@ export default {
scrollbarThumb.style.top = `${Math.min(thumbTop, maxTop)}px`; scrollbarThumb.style.top = `${Math.min(thumbTop, maxTop)}px`;
}, },
handleScroll() { handleScroll() {
const container = this.$refs.tableContainer;
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
if (this.currentPage * this.pageSize < this.total) {
this.currentPage++;
this.loadData();
}
}
this.updateThumbPosition(); this.updateThumbPosition();
}, },
startDrag(e) { startDrag(e) {
this.isDragging = true; this.isDragging = true;
this.startY = e.clientY; this.startY = e.clientY;
this.scrollTop = this.$refs.tableContainer.scrollTop; this.scrollTop = this.$refs.tableContainer.scrollTop;
e.preventDefault(); e.preventDefault();
}, },
stopDrag() { stopDrag() {
this.isDragging = false; this.isDragging = false;
}, },
handleDrag(e) { handleDrag(e) {
if (!this.isDragging) return; if (!this.isDragging) return;
@@ -249,6 +301,7 @@ export default {
const scrollRatio = (trackHeight - thumbHeight) / maxScrollTop; const scrollRatio = (trackHeight - thumbHeight) / maxScrollTop;
container.scrollTop = this.scrollTop + deltaY / scrollRatio; container.scrollTop = this.scrollTop + deltaY / scrollRatio;
}, },
handleTrackClick(e) { handleTrackClick(e) {
const container = this.$refs.tableContainer; const container = this.$refs.tableContainer;
const scrollbarTrack = this.$refs.scrollbarTrack; const scrollbarTrack = this.$refs.scrollbarTrack;
@@ -268,16 +321,75 @@ export default {
scrollbarThumb.style.top = `${newTop}px`; scrollbarThumb.style.top = `${newTop}px`;
container.scrollTop = (newTop / (trackHeight - thumbHeight)) * (container.scrollHeight - container.clientHeight); container.scrollTop = (newTop / (trackHeight - thumbHeight)) * (container.scrollHeight - container.clientHeight);
}, },
// 按钮组
startEdit(row) { startEdit(row) {
row.editing = true; row.editing = true;
this.$set(row, 'originalData', {...row}); this.$set(row, 'originalData', {...row});
}, },
saveEdit(row) { saveEdit(row) {
row.editing = false; try {
delete row.originalData; 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
};
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
});
}
}, },
toggleSelectAll() { toggleSelectAll() {
@@ -288,24 +400,128 @@ export default {
}, },
addNew() { addNew() {
const maxSort = this.ttsModels.length > 0
? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0))
: 0;
const newRow = { const newRow = {
voiceCode: '新编码', voiceCode: '',
voiceName: '新音色', voiceName: '',
languageType: '中文', languageType: '中文',
voiceDemo: '',
remark: '', remark: '',
selected: false, selected: false,
editing: true editing: true,
sort: maxSort + 1
}; };
this.ttsModels.unshift(newRow); this.ttsModels.unshift(newRow);
}, },
deleteRow(row) { deleteRow(row) {
const index = this.ttsModels.indexOf(row); this.$confirm("确定要删除该音色吗?", "警告", {
this.ttsModels.splice(index, 1); confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
Api.timbre.deleteVoice(row.id, (response) => {
if (response.code === 0) {
this.$message.success({
message: "删除成功",
showClose: true
});
this.loadData(); // 刷新数据
} else {
this.$message.error({
message: response.msg || "删除失败",
showClose: true
});
}
});
})
.catch(() => {
this.$message.info("已取消删除");
});
}, },
batchDelete() { batchDelete() {
this.ttsModels = this.ttsModels.filter(row => !row.selected); const selectedRows = this.filteredTtsModels.filter(row => row.selected);
if (selectedRows.length === 0) {
this.$message.warning("请先选择需要删除的音色");
return;
}
this.$confirm(`确定要删除选中的${selectedRows.length}个音色吗?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
const loading = this.$loading({
lock: true,
text: "正在删除中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
try {
const results = await Promise.all(
selectedRows.map((row) => {
return new Promise((resolve) => {
Api.timbre.deleteVoice(row.id, (response) => {
if (response.code === 0) {
resolve({ success: true, id: row.id });
} else {
resolve({
success: false,
id: row.id,
msg: response.msg || '删除失败'
});
}
});
});
})
);
const successCount = results.filter(r => r.success).length;
const failCount = results.length - successCount;
if (failCount === 0) {
this.$message.success({
message: `成功删除${successCount}个音色`,
showClose: true
});
} else if (successCount === 0) {
this.$message.error({
message: '删除失败,请重试',
showClose: true
});
} else {
this.$message.warning({
message: `成功删除${successCount}个音色,${failCount}个删除失败`,
showClose: true
});
}
this.loadData(); // 刷新数据
} catch (error) {
console.error('批量删除出错:', error);
this.$message.error({
message: '删除过程中发生错误',
showClose: true
});
} finally {
loading.close();
}
})
.catch(() => {
this.$message.info("已取消删除");
});
},
isValidAudioUrl(url) {
return url && (url.endsWith('.mp3') || url.endsWith('.ogg') || url.endsWith('.wav'));
} }
} }
}; };
+11 -2
View File
@@ -80,7 +80,11 @@
</el-table-column> </el-table-column>
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center"> <el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" size="mini" @click="ttsDialogVisible = true" class="voice-management-btn"> <el-button
type="text"
size="mini"
@click="openTtsDialog(scope.row)"
class="voice-management-btn">
音色管理 音色管理
</el-button> </el-button>
</template> </template>
@@ -124,7 +128,7 @@
<ModelEditDialog :modelType="activeTab" :visible.sync="editDialogVisible" :modelData="editModelData" <ModelEditDialog :modelType="activeTab" :visible.sync="editDialogVisible" :modelData="editModelData"
@save="handleModelSave" /> @save="handleModelSave" />
<TtsModel :visible.sync="ttsDialogVisible" /> <TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId"/>
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" /> <AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
</div> </div>
@@ -151,6 +155,7 @@ export default {
editDialogVisible: false, editDialogVisible: false,
editModelData: {}, editModelData: {},
ttsDialogVisible: false, ttsDialogVisible: false,
selectedTtsModelId: '',
modelList: [], modelList: [],
currentPage: 1, currentPage: 1,
pageSize: 5, pageSize: 5,
@@ -200,6 +205,10 @@ export default {
}, },
methods: { methods: {
openTtsDialog(row) {
this.selectedTtsModelId = row.id;
this.ttsDialogVisible = true;
},
headerCellClassName({ column, columnIndex }) { headerCellClassName({ column, columnIndex }) {
if (columnIndex === 0) { if (columnIndex === 0) {
return 'custom-selection-header'; return 'custom-selection-header';