VoiceManagement.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. <template>
  2. <div class="voice-management">
  3. <div class="header">
  4. <h2>Voice Management</h2>
  5. <div class="header-actions">
  6. <!-- <el-upload-->
  7. <!-- class="import-btn"-->
  8. <!-- action="javascript:;"-->
  9. <!-- :show-file-list="false"-->
  10. <!-- :before-upload="handleExcelUpload"-->
  11. <!-- accept=".xlsx,.xls"-->
  12. <!-- >-->
  13. <!-- <el-button type="text">-->
  14. <!-- <el-icon><Upload /></el-icon>Import-->
  15. <!-- </el-button>-->
  16. <!-- </el-upload>-->
  17. <!-- <el-button type="primary" @click="showAddDialog">-->
  18. <!-- Add Voice-->
  19. <!-- </el-button>-->
  20. </div>
  21. </div>
  22. <el-table :data="voices" v-loading="loading" style="width: 100%" class="voice-table">
  23. <el-table-column label="Voice Name" min-width="200">
  24. <template #default="scope">
  25. <div class="voice-info">
  26. <!-- <div class="voice-icon">-->
  27. <!-- <el-icon><Microphone /></el-icon>-->
  28. <!-- </div>-->
  29. <div class="voice-name">{{ scope.row.name }}</div>
  30. </div>
  31. </template>
  32. </el-table-column>
  33. <!-- <el-table-column prop="description" label="Voice Description" min-width="250" />-->
  34. <el-table-column prop="gender" label="Gender" width="200">
  35. <template #default="scope">
  36. {{ scope.row.gender === '1' ? 'Male' : 'Female' }}
  37. </template>
  38. </el-table-column>
  39. <!-- <el-table-column label="Original Voice" width="150">-->
  40. <!-- <template #default="scope">-->
  41. <!-- <el-button type="text" @click="playVoice(scope.row.originalVoice)">-->
  42. <!-- <el-icon><VideoPlay /></el-icon> 试听-->
  43. <!-- </el-button>-->
  44. <!-- </template>-->
  45. <!-- </el-table-column>-->
  46. <el-table-column prop="createTime" label="create Date" width="120" >
  47. <template #default="scope">
  48. {{ scope.row.ctime}}
  49. </template>
  50. </el-table-column>
  51. <!-- <el-table-column label="Actions" width="150">-->
  52. <!-- <template #default="scope">-->
  53. <!-- <div class="action-buttons">-->
  54. <!-- <el-button type="text" @click="editVoice(scope.row)">-->
  55. <!-- <el-icon><Edit /></el-icon>-->
  56. <!-- </el-button>-->
  57. <!-- <el-button type="text" class="delete-btn" @click="confirmDelete(scope.row)">-->
  58. <!-- <el-icon><Delete /></el-icon>-->
  59. <!-- </el-button>-->
  60. <!-- </div>-->
  61. <!-- </template>-->
  62. <!-- </el-table-column>-->
  63. </el-table>
  64. <!-- 导入确认对话框 -->
  65. <el-dialog
  66. title="导入声音"
  67. v-model="importDialogVisible"
  68. width="500px"
  69. >
  70. <div v-if="importFileName" class="import-file-info">
  71. 即将导入文件: {{ importFileName }}
  72. </div>
  73. <div v-else>
  74. 请先选择Excel文件
  75. </div>
  76. <template #footer>
  77. <el-button @click="importDialogVisible = false">取消</el-button>
  78. <el-button
  79. type="primary"
  80. @click="confirmImport"
  81. :disabled="!importFileName"
  82. >
  83. 确认导入
  84. </el-button>
  85. </template>
  86. </el-dialog>
  87. <!-- Add Voice Dialog -->
  88. <el-dialog
  89. :title="dialogType === 'add' ? 'Add Voice' : 'Edit Voice'"
  90. v-model="dialogVisible"
  91. width="500px"
  92. class="voice-dialog"
  93. >
  94. <el-form
  95. ref="voiceFormRef"
  96. :model="voiceForm"
  97. :rules="rules"
  98. label-width="120px"
  99. >
  100. <el-form-item label="Voice Name" prop="name">
  101. <el-input v-model="voiceForm.name" placeholder="Enter voice name" />
  102. </el-form-item>
  103. <el-form-item label="Description" prop="description">
  104. <el-input
  105. v-model="voiceForm.description"
  106. type="textarea"
  107. placeholder="Enter voice description"
  108. rows="3"
  109. />
  110. </el-form-item>
  111. <el-form-item label="Gender" prop="gender">
  112. <el-select v-model="voiceForm.gender" placeholder="Select gender" style="width: 100%">
  113. <el-option label="Male" value="male" />
  114. <el-option label="Female" value="female" />
  115. </el-select>
  116. </el-form-item>
  117. <!-- <el-form-item label="Original Voice" prop="originalVoice">-->
  118. <!-- <el-upload-->
  119. <!-- :show-file-list="false"-->
  120. <!-- :before-upload="beforeUploadVoice"-->
  121. <!-- :on-success="(response, file) => handleVoiceUploadSuccess(file, 'originalVoice')"-->
  122. <!-- :on-error="handleUploadError"-->
  123. <!-- class="upload-voice"-->
  124. <!-- >-->
  125. <!-- <el-button plain>-->
  126. <!-- <el-icon><Upload /></el-icon>-->
  127. <!-- {{ voiceForm.originalVoice ? 'Change File' : 'Upload File' }}-->
  128. <!-- </el-button>-->
  129. <!-- </el-upload>-->
  130. <!-- <div v-if="voiceForm.originalVoice" class="file-name">-->
  131. <!-- {{ voiceForm.originalVoice }}-->
  132. <!-- <el-button type="text" size="small" @click="removeVoiceFile('originalVoice')">-->
  133. <!-- <el-icon><CircleClose /></el-icon>-->
  134. <!-- </el-button>-->
  135. <!-- </div>-->
  136. <!-- </el-form-item>-->
  137. <el-form-item label="Cloned Voice" prop="clonedVoice">
  138. <div v-if="voiceForm.originalVoice && !voiceForm.clonedVoice" class="cloning-status">
  139. <el-icon><Loading /></el-icon>
  140. <span>正在从原始声音源文件克隆...</span>
  141. </div>
  142. <div v-else-if="voiceForm.clonedVoice" class="file-name">
  143. {{ voiceForm.clonedVoice }}
  144. <el-button type="text" size="small" @click="removeVoiceFile('clonedVoice')">
  145. <el-icon><CircleClose /></el-icon>
  146. </el-button>
  147. </div>
  148. <div v-else>
  149. <span class="no-clone-hint">请先上传原始声音源文件</span>
  150. </div>
  151. </el-form-item>
  152. </el-form>
  153. <template #footer>
  154. <el-button @click="dialogVisible = false">Cancel</el-button>
  155. <el-button type="primary" @click="submitForm">
  156. {{ dialogType === 'add' ? 'Add' : 'Save Changes' }}
  157. </el-button>
  158. </template>
  159. </el-dialog>
  160. <Pagination
  161. :current-page="currentPage"
  162. :per-page="perPage"
  163. :total-items="totalItems"
  164. :max-displayed-pages="maxDisplayedPages"
  165. :show-info="true"
  166. @page-changed="onPageChange"
  167. />
  168. </div>
  169. </template>
  170. <script>
  171. import { Edit, Delete, VideoPlay, Upload, Microphone, CircleClose, Loading } from '@element-plus/icons-vue'
  172. import { ElMessage, ElMessageBox } from 'element-plus'
  173. import { useStore } from '../store'
  174. import Pagination from './util/Pagination.vue'
  175. import anycallService from "../service/anycallService";
  176. export default {
  177. name: 'VoiceManagement',
  178. components: {
  179. Pagination,
  180. Edit,
  181. Delete,
  182. VideoPlay,
  183. Upload,
  184. CircleClose,
  185. Loading,
  186. Microphone: {
  187. template: `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa="">
  188. <path fill="currentColor" d="M512 128a128 128 0 0 0-128 128v256a128 128 0 1 0 256 0V256a128 128 0 0 0-128-128zm0 64a64 64 0 0 1 64 64v256a64 64 0 1 1-128 0V256a64 64 0 0 1 64-64zM320 512a32 32 0 0 0-32 32v64a224 224 0 0 0 448 0v-64a32 32 0 0 0-64 0v64a160 160 0 0 1-320 0v-64a32 32 0 0 0-32-32z"></path>
  189. </svg>`
  190. }
  191. },
  192. data() {
  193. const store = useStore()
  194. return {
  195. currentPage: 1,
  196. perPage: 10,
  197. totalItems: store.voicesCount,
  198. maxDisplayedPages: 5,
  199. voices: store.voices,
  200. loading: false,
  201. dialogVisible: false,
  202. dialogType: 'add', // 'add' or 'edit'
  203. importDialogVisible: false,
  204. importFileName: '',
  205. voiceForm: {
  206. id: '',
  207. description: '',
  208. gender: '',
  209. originalVoice: '',
  210. clonedVoice: '',
  211. uploadTime: ''
  212. },
  213. currentVoice: null,
  214. rules: {
  215. name: [
  216. { required: true, message: 'Please enter voice name', trigger: 'blur' }
  217. ],
  218. description: [
  219. { required: true, message: 'Please enter voice description', trigger: 'blur' }
  220. ],
  221. gender: [
  222. { required: true, message: 'Please select gender', trigger: 'change' }
  223. ],
  224. originalVoice: [
  225. { required: true, message: 'Please upload original voice file', trigger: 'blur' }
  226. ]
  227. }
  228. }
  229. },
  230. methods: {
  231. async onPageChange(page) {
  232. this.currentPage = page
  233. // 这里可以添加加载数据的逻辑
  234. let pageData = {
  235. page: page,
  236. size: this.perPage,
  237. system: true,
  238. gender: 1
  239. };
  240. const response = await anycallService.voiceList(pageData);
  241. this.voices = response.data.data.content.map(item => ({
  242. id: item.id || '',
  243. name: item.name || '',
  244. gender: item.gender || '',
  245. ctime: new Date(item.ctime).getFullYear()+'-'+new Date(item.ctime).getMonth()+'-'+new Date(item.ctime).getDay() || '',
  246. }
  247. ));
  248. this.rolesCount = response.data.data.total;
  249. },
  250. // 处理Excel上传
  251. handleExcelUpload(file) {
  252. this.importDialogVisible = true;
  253. return false; // 阻止默认上传
  254. },
  255. // 确认导入
  256. confirmImport() {
  257. // 模拟导入处理
  258. ElMessage.success(`文件 ${this.importFileName} 导入成功`);
  259. this.importDialogVisible = false;
  260. this.importFileName = '';
  261. // 实际项目中应解析Excel文件并添加声音
  262. },
  263. // Play voice (simulation)
  264. playVoice(filename) {
  265. ElMessage.info(`Playing voice file: ${filename} (simulation)`)
  266. },
  267. // Show add dialog
  268. showAddDialog() {
  269. this.dialogType = 'add'
  270. this.voiceForm = {
  271. id: '',
  272. description: '',
  273. gender: '',
  274. originalVoice: '',
  275. clonedVoice: '',
  276. uploadTime: ''
  277. }
  278. this.dialogVisible = true
  279. },
  280. // Edit voice
  281. editVoice(voice) {
  282. this.dialogType = 'edit'
  283. this.currentVoice = voice
  284. this.voiceForm = { ...voice }
  285. this.dialogVisible = true
  286. },
  287. // Confirm delete
  288. confirmDelete(voice) {
  289. ElMessageBox.confirm(
  290. `Are you sure you want to delete voice "${voice.description}"?`,
  291. 'Warning',
  292. {
  293. confirmButtonText: 'Delete',
  294. cancelButtonText: 'Cancel',
  295. type: 'warning'
  296. }
  297. ).then(() => {
  298. const store = useStore()
  299. store.deleteVoice(voice.id)
  300. this.voices = store.voices
  301. ElMessage.success('Voice deleted successfully')
  302. }).catch(() => {})
  303. },
  304. // 上传前检查 - 只接受原始声音源文件
  305. beforeUploadVoice(file) {
  306. const isAudio = ['audio/mpeg', 'audio/wav', 'audio/mp3'].includes(file.type) || file.name.endsWith('.mp3') || file.name.endsWith('.wav');
  307. const isLt20M = file.size / 1024 / 1024 < 20;
  308. if (!isAudio) {
  309. ElMessage.error('请上传音频格式文件!');
  310. }
  311. if (!isLt20M) {
  312. ElMessage.error('上传文件大小不能超过20MB!');
  313. }
  314. return isAudio && isLt20M;
  315. },
  316. // 处理上传成功
  317. handleVoiceUploadSuccess(file, field) {
  318. // 模拟上传成功,实际应用中应该根据服务器返回设置文件路径
  319. this.voiceForm[field] = file.name;
  320. ElMessage.success(`${field === 'originalVoice' ? '原始' : '克隆'} 声音文件上传成功`);
  321. // 如果上传的是原始声音,模拟系统自动克隆过程
  322. if (field === 'originalVoice' && !this.voiceForm.clonedVoice) {
  323. // 模拟克隆过程的延迟
  324. setTimeout(() => {
  325. // 生成克隆文件名
  326. const originalFileName = file.name;
  327. const clonedFileName = originalFileName.replace('original-', 'cloned-');
  328. this.voiceForm.clonedVoice = clonedFileName;
  329. ElMessage.success('已从原始声音源文件成功克隆声音');
  330. }, 2000); // 模拟2秒的克隆时间
  331. }
  332. },
  333. // 处理上传错误
  334. handleUploadError() {
  335. ElMessage.error('File upload failed');
  336. },
  337. // 移除已上传的文件
  338. removeVoiceFile(field) {
  339. this.voiceForm[field] = '';
  340. },
  341. // Submit form
  342. submitForm() {
  343. this.$refs.voiceFormRef.validate((valid) => {
  344. if (valid) {
  345. const store = useStore()
  346. if (this.dialogType === 'add') {
  347. store.addVoice({
  348. ...this.voiceForm,
  349. id: Date.now().toString(),
  350. uploadTime: new Date().toLocaleDateString()
  351. })
  352. ElMessage.success('Voice added successfully')
  353. } else {
  354. store.updateVoice(this.currentVoice.id, this.voiceForm)
  355. ElMessage.success('Voice updated successfully')
  356. }
  357. this.voices = store.voices
  358. this.dialogVisible = false
  359. }
  360. })
  361. }
  362. }
  363. }
  364. </script>
  365. <style>
  366. .voice-management {
  367. padding: 20px;
  368. }
  369. .header {
  370. display: flex;
  371. justify-content: space-between;
  372. align-items: center;
  373. margin-bottom: 20px;
  374. }
  375. .header h2 {
  376. font-size: 24px;
  377. font-weight: 600;
  378. margin: 0;
  379. }
  380. .header-actions {
  381. display: flex;
  382. gap: 10px;
  383. }
  384. .import-btn {
  385. border-color: #dcdfe6;
  386. }
  387. .voice-table {
  388. margin-top: 20px;
  389. border-radius: 8px;
  390. overflow: hidden;
  391. }
  392. .voice-info {
  393. display: flex;
  394. align-items: center;
  395. gap: 12px;
  396. }
  397. .voice-icon {
  398. width: 40px;
  399. height: 40px;
  400. border-radius: 50%;
  401. background-color: #f0f2f5;
  402. display: flex;
  403. align-items: center;
  404. justify-content: center;
  405. color: #409eff;
  406. }
  407. .voice-details {
  408. display: flex;
  409. flex-direction: column;
  410. }
  411. .voice-name {
  412. font-weight: 500;
  413. }
  414. .voice-id {
  415. font-size: 12px;
  416. color: #909399;
  417. }
  418. .action-buttons {
  419. display: flex;
  420. gap: 10px;
  421. }
  422. .delete-btn {
  423. color: #f56c6c;
  424. }
  425. .upload-box {
  426. display: flex;
  427. align-items: center;
  428. gap: 10px;
  429. }
  430. .file-name {
  431. color: #909399;
  432. font-size: 14px;
  433. }
  434. .voice-dialog .el-form-item {
  435. margin-bottom: 20px;
  436. }
  437. .upload-voice {
  438. display: inline-block;
  439. }
  440. .file-name {
  441. margin-top: 10px;
  442. display: flex;
  443. align-items: center;
  444. gap: 10px;
  445. font-size: 14px;
  446. color: #606266;
  447. }
  448. .cloning-status {
  449. display: flex;
  450. align-items: center;
  451. gap: 10px;
  452. color: #409eff;
  453. font-size: 14px;
  454. }
  455. .no-clone-hint {
  456. color: #909399;
  457. font-size: 14px;
  458. }
  459. .voice-info {
  460. display: flex;
  461. align-items: center;
  462. gap: 10px;
  463. }
  464. .voice-name {
  465. font-weight: 500;
  466. }
  467. </style>