feat: 语音智能体
This commit is contained in:
parent
eebd0dc6b7
commit
ecd13e2494
46
src/api/corpusManger/speech_synthesis.js
Normal file
46
src/api/corpusManger/speech_synthesis.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function taskApi(data) {
|
||||||
|
return request({
|
||||||
|
url: '/tts/task',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function taskListApi(params) {
|
||||||
|
return request({
|
||||||
|
url: '/tts/task/list',
|
||||||
|
method: 'get',
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDashboardApi(params) {
|
||||||
|
return request({
|
||||||
|
url: '/tts/task/dashboard/stats',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadWavApi(id) {
|
||||||
|
return request({
|
||||||
|
url: `/tts/task/audio/${id}`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTask(ids) {
|
||||||
|
return request({
|
||||||
|
url: `/tts/task/${ids}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function regenerateApi(id) {
|
||||||
|
return request({
|
||||||
|
url: `/tts/task/regenerate/${id}`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
4
src/assets/icons/svg/add.svg
Normal file
4
src/assets/icons/svg/add.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg t="1783317138800" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12777" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||||
|
<path d="M898.004474 894.186428 798.735272 894.186428 798.735272 993.45563C798.735272 1009.90301 785.401891 1023.236391 768.954512 1023.236391 752.507132 1023.236391 739.173751 1009.90301 739.173751 993.45563L739.173751 894.186428 639.904549 894.186428C623.457169 894.186428 610.123788 880.853047 610.123788 864.405667 610.123788 847.958288 623.457169 834.624907 639.904549 834.624907L739.173751 834.624907 739.173751 735.355705C739.173751 718.908325 752.507132 705.574944 768.954512 705.574944 785.401891 705.574944 798.735272 718.908325 798.735272 735.355705L798.735272 834.624907 898.004474 834.624907C914.451854 834.624907 927.785235 847.958288 927.785235 864.405667 927.785235 880.853047 914.451854 894.186428 898.004474 894.186428ZM768.954512 641.049963C752.507132 641.049963 739.173751 627.716582 739.173751 611.269202L739.173751 60.32513 415.012081 60.32513C415.013608 60.452653 415.021626 60.579031 415.021626 60.706935L415.021626 291.316928C415.021626 307.764307 401.688245 321.097688 385.240865 321.097688L155.776286 321.097688 155.776286 836.152125 515.054437 836.152125C531.501817 836.152125 544.835198 849.485506 544.835198 865.932886 544.835198 882.380265 531.501817 895.713647 515.054437 895.713647L125.995526 895.713647C109.548146 895.713647 96.214765 882.380265 96.214765 865.932886L96.214765 242.064131C96.214765 237.412987 97.282291 233.011544 99.18406 229.089647 99.90567 226.826309 101.078574 224.786327 102.737515 223.086151L309.85087 10.837906C310.512919 10.088805 311.210476 9.372158 311.943541 8.693309L314.300802 6.277632C317.094849 3.414097 320.879296 1.89146 325.120764 1.610452 327.381429 1.061417 329.7406 0.763609 332.170022 0.763609L768.954512 0.763609C785.401891 0.763609 798.735272 14.09699 798.735272 30.54437L798.735272 611.269202C798.735272 627.716582 785.401891 641.049963 768.954512 641.049963ZM344.780647 60.32513 155.776286 254.015761 155.776286 261.536167 355.460104 261.536167 355.460104 60.706935C355.460104 60.579031 355.468122 60.452653 355.46965 60.32513L344.780647 60.32513Z" p-id="12778" fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
1
src/assets/icons/svg/consuming.svg
Normal file
1
src/assets/icons/svg/consuming.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783317313942" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17054" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M672.881778 502.215111a225.28 225.28 0 0 1-77.141334 50.688A225.450667 225.450667 0 0 1 512 568.888889a225.450667 225.450667 0 0 1-83.740444-15.928889 225.28 225.28 0 0 1-77.141334-50.744889 225.223111 225.223111 0 0 1-50.688-77.141333A225.564444 225.564444 0 0 1 284.444444 341.333333c0-28.672 5.347556-57.116444 15.928889-83.740444 11.605333-29.240889 28.501333-54.954667 50.744889-77.141333a225.28 225.28 0 0 1 77.141334-50.688A225.564444 225.564444 0 0 1 512 113.777778c28.672 0 57.116444 5.347556 83.740444 15.928889 29.240889 11.605333 54.954667 28.501333 77.141334 50.744889 22.186667 22.186667 39.139556 47.900444 50.688 77.141333 10.638222 26.624 16.042667 55.068444 15.985778 83.740444 0 28.672-5.404444 57.116444-15.928889 83.740445a225.28 225.28 0 0 1-50.744889 77.141333z m-50.801778-270.961778a154.168889 154.168889 0 0 0-52.792889-34.702222A154.339556 154.339556 0 0 0 512 185.628444c-19.626667 0-39.082667 3.697778-57.287111 10.922667a154.112 154.112 0 0 0-52.792889 34.702222c-15.075556 15.018667-26.908444 32.995556-34.702222 52.792889-7.224889 18.204444-10.922667 37.660444-10.922667 57.287111 0 19.797333 3.640889 38.912 10.922667 57.287111 7.793778 19.797333 19.626667 37.774222 34.702222 52.792889 15.018667 15.075556 32.995556 26.908444 52.792889 34.702223 18.204444 7.224889 37.660444 10.979556 57.287111 10.922666 19.626667 0 39.082667-3.697778 57.287111-10.922666 19.797333-7.793778 37.774222-19.626667 52.792889-34.702223 15.075556-15.018667 26.908444-32.995556 34.702222-52.792889 7.224889-18.204444 10.922667-37.660444 10.922667-57.287111 0-19.626667-3.697778-39.082667-10.922667-57.287111a153.998222 153.998222 0 0 0-34.702222-52.792889z" p-id="17055" fill="currentColor"></path><path d="M228.750222 616.220444a388.152889 388.152889 0 0 1 122.481778-88.177777 368.64 368.64 0 0 1 134.371556-33.052445 33.678222 33.678222 0 0 1 25.144888 9.671111c6.883556 6.599111 10.979556 15.928889 11.434667 25.770667a38.912 38.912 0 0 1-8.931555 26.851556 34.133333 34.133333 0 0 1-24.177778 12.288 302.819556 302.819556 0 0 0-110.364445 27.192888 318.691556 318.691556 0 0 0-100.579555 72.362667 342.357333 342.357333 0 0 0-68.380445 106.951111 359.992889 359.992889 0 0 0-26.055111 117.703111c-1.024 20.593778-17.578667 36.408889-36.864 35.271111a34.133333 34.133333 0 0 1-24.120889-12.401777 38.912 38.912 0 0 1-8.874666-26.965334 438.385778 438.385778 0 0 1 31.687111-143.246222 416.938667 416.938667 0 0 1 83.228444-130.275555z m426.894222 333.824c-19.342222 0-34.986667-16.725333-34.986666-37.319111v-231.537777c0-20.650667 15.644444-37.319111 34.986666-37.319112 19.342222 0 34.986667 16.668444 34.986667 37.319112v231.537777c0 20.593778-15.644444 37.319111-34.986667 37.319111z m-111.957333-61.610666c-19.342222 0-34.986667-16.725333-34.986667-37.319111V744.675556c0-20.650667 15.644444-37.376 34.986667-37.376 19.342222 0 34.986667 16.725333 34.986667 37.376v106.382222c0 20.650667-15.701333 37.376-34.986667 37.376z m223.971556 0c-19.342222 0-34.986667-16.725333-34.986667-37.319111V744.675556c0-20.650667 15.644444-37.376 34.986667-37.376 19.342222 0 34.986667 16.725333 34.986666 37.376v106.382222c0 20.650667-15.701333 37.376-34.986666 37.376zM967.111111 797.013333c0 20.593778-15.644444 37.319111-34.986667 37.319111h-45.511111c-19.342222 0-34.986667-16.725333-34.986666-37.319111 0-20.650667 15.644444-37.376 34.986666-37.376h45.511111c19.342222 0 34.986667 16.725333 34.986667 37.376z" p-id="17056" fill="currentColor"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@ -1 +1 @@
|
|||||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783317252138" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16006" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 64.194C264.707 64.194 64.194 264.707 64.194 512S264.707 959.806 512 959.806 959.806 759.293 959.806 512 759.293 64.194 512 64.194z m0 223.903a497.77 497.77 0 0 1-86.163-7.497C452.026 175.446 491.409 120.17 512 120.17s59.974 55.276 86.163 160.43c-27.888 4.798-56.676 7.497-86.163 7.497z m140.639-20.391c-13.594-55.676-31.786-102.556-53.077-137.34 63.572 14.594 121.247 44.681 168.927 86.063-35.285 21.79-74.268 39.082-115.85 51.277z m-281.278 0c-41.582-12.195-80.465-29.587-115.75-51.278 47.679-41.382 105.254-71.369 168.827-85.963-21.291 34.685-39.383 81.565-53.077 137.241z m308.066 216.306c-1.299-57.475-6.697-111.852-15.393-161.33 52.477-14.993 101.256-37.284 144.737-65.772 53.377 61.973 87.462 140.639 93.659 227.102H679.427z m-557.858 0c6.197-86.363 40.183-165.028 93.459-227.002 43.481 28.488 92.36 50.578 144.937 65.572-8.696 49.578-14.094 103.955-15.393 161.43H121.569z m278.979 0c1.299-55.676 6.397-105.354 13.894-148.636 31.586 5.598 64.072 8.696 97.558 8.696 33.386 0 65.971-2.999 97.558-8.596 7.497 43.281 12.495 92.96 13.894 148.536H400.548z m408.223 283.077c-43.481-28.488-92.26-50.778-144.737-65.771 8.696-49.578 14.094-103.955 15.393-161.33H902.43c-6.197 86.462-40.282 165.128-93.659 227.101z m-394.329-78.466c-7.497-43.281-12.495-92.96-13.894-148.636h222.803c-1.299 55.576-6.297 105.254-13.894 148.536-31.486-5.598-64.072-8.596-97.458-8.596s-65.97 3.099-97.557 8.696z m-199.313 78.366c-53.277-61.973-87.362-140.639-93.459-227.002h223.003c1.299 57.475 6.697 111.852 15.393 161.43-52.577 14.994-101.456 37.085-144.937 65.572zM512 903.83c-20.591 0-59.974-55.276-86.163-160.43 27.888-4.798 56.675-7.497 86.163-7.497 29.487 0 58.275 2.699 86.163 7.497C571.974 848.554 532.591 903.83 512 903.83z m87.562-10.196c21.291-34.785 39.483-81.665 53.077-137.34 41.682 12.195 80.565 29.487 115.85 51.178-47.68 41.482-105.355 71.569-168.927 86.162z m-175.124 0c-63.572-14.594-121.147-44.581-168.827-85.963 35.185-21.791 74.068-39.083 115.75-51.278 13.694 55.577 31.786 102.457 53.077 137.241z" p-id="16007" fill="currentColor"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.3 KiB |
5
src/assets/icons/svg/task.svg
Normal file
5
src/assets/icons/svg/task.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg t="1783317214101" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14894" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M799.197483 1024H224.802517A130.013483 130.013483 0 0 1 94.881079 894.067056V227.374022A130.013483 130.013483 0 0 1 224.802517 97.452584h47.65627v64.960719h-47.65627a65.018247 65.018247 0 0 0-64.972225 64.960719v666.693034a65.018247 65.018247 0 0 0 64.960719 64.960719h574.406472a65.018247 65.018247 0 0 0 64.960719-64.960719V227.374022a65.018247 65.018247 0 0 0-64.960719-64.960719h-42.099056V97.452584h42.099056a130.013483 130.013483 0 0 1 129.921438 129.921438v666.693034A130.013483 130.013483 0 0 1 799.197483 1024z" fill="currentColor" p-id="14895"></path><path d="M644.308854 259.854382h-264.629214a129.932944 129.932944 0 0 1 0-259.854382h264.629214a129.932944 129.932944 0 0 1 0 259.854382z m-264.629214-194.893663a64.972225 64.972225 0 0 0 0 129.932944h264.629214a64.972225 64.972225 0 0 0 0-129.932944z" fill="currentColor" p-id="14896"></path>
|
||||||
|
<path d="M241.796315 374.910562m32.480359 0l475.446652 0q32.48036 0 32.480359 32.480359l0 0q0 32.48036-32.480359 32.48036l-475.446652 0q-32.48036 0-32.480359-32.48036l0 0q0-32.48036 32.480359-32.480359Z" fill="currentColor" p-id="14897"></path><path d="M241.796315 541.845573m32.480359 0l475.446652 0q32.48036 0 32.480359 32.48036l0 0q0 32.48036-32.480359 32.480359l-475.446652 0q-32.48036 0-32.480359-32.480359l0 0q0-32.48036 32.480359-32.48036Z" fill="currentColor" p-id="14898"></path><path d="M241.796315 708.780584m32.480359 0l475.446652 0q32.48036 0 32.480359 32.48036l0 0q0 32.48036-32.480359 32.480359l-475.446652 0q-32.48036 0-32.480359-32.480359l0 0q0-32.48036 32.480359-32.48036Z" fill="currentColor" p-id="14899"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
4
src/assets/icons/svg/timbre.svg
Normal file
4
src/assets/icons/svg/timbre.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg t="1783317427036" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19916" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||||
|
<path d="M874.18 149.92C777.456 53.254 648.91 0 512.102 0 375.343 0 246.78 53.255 150.097 149.93 53.38 246.635 0.118 375.225 0.118 512.003 0.094 648.796 53.314 777.357 150.02 874.122 246.69 970.736 375.266 1024 512.007 1024c136.807 0 265.425-53.298 362.172-149.979 199.588-199.608 199.619-524.417 0-724.102z m-47.186 676.907C742.864 910.946 631 957.32 512.008 957.32c-118.907 0-230.744-46.35-314.82-130.392-84.077-84.093-130.368-195.934-130.35-314.924 0-118.973 46.314-230.826 130.42-314.899C281.354 113.03 393.154 66.731 512.104 66.731c118.955 0 230.779 46.297 314.891 130.38 173.59 173.631 173.59 456.144 0 629.716z m-93.643-189.351L556.37 535.125V256c0-23.557-19.111-42.667-42.674-42.667-23.545 0-42.656 19.11-42.656 42.667v298.67c0 15.826 19.671 29.096 32.468 36.437 3.538 5.33 13.268 10.153 19.2 13.565l176 106.673c20.388 11.77 42.495 4.775 54.27-15.62 11.776-20.4 0.812-46.469-19.63-58.244v-0.005z m0 0" p-id="19917" fill="currentColor"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
83
src/views/corpusManger/corpus/components/CategoryTree.vue
Normal file
83
src/views/corpusManger/corpus/components/CategoryTree.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<el-card shadow="never" class="category-tree">
|
||||||
|
<div class="tree-header">
|
||||||
|
<span class="stat-label">语料库目录</span>
|
||||||
|
<el-button size="small" type="primary" plain>+新建分类</el-button>
|
||||||
|
</div>
|
||||||
|
<el-tree
|
||||||
|
:data="treeData"
|
||||||
|
:props="defaultProps"
|
||||||
|
default-expand-all
|
||||||
|
highlight-current
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['select-category'])
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
children: 'children',
|
||||||
|
label: 'label'
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = ref([
|
||||||
|
{
|
||||||
|
label: '全部语料',
|
||||||
|
count: '1,245,678',
|
||||||
|
children: [
|
||||||
|
{ label: '控制指令', count: '245,678', children: [
|
||||||
|
{ label: '车窗控制', count: '82,345' },
|
||||||
|
{ label: '空调控制', count: '51,234' },
|
||||||
|
{ label: '座椅调节', count: '32,145' }
|
||||||
|
]},
|
||||||
|
{ label: '导航交互', count: '183,456' },
|
||||||
|
{ label: '媒体娱乐', count: '98,765' },
|
||||||
|
{ label: '电话消息', count: '124,567' },
|
||||||
|
{ label: '场景对话', count: '156,789', children: [
|
||||||
|
{ label: '日常对话', count: '78,654' },
|
||||||
|
{ label: '出行场景', count: '78,135' }
|
||||||
|
]},
|
||||||
|
{ label: '系统设置', count: '21,345' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleNodeClick = (data) => {
|
||||||
|
emit('select-category', data)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.category-tree {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.tree-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
:deep(.el-tree-node__content) {
|
||||||
|
height: 32px;
|
||||||
|
.el-tree-node__label {
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
&::after {
|
||||||
|
content: attr(data-count);
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
164
src/views/corpusManger/corpus/components/CorpusList.vue
Normal file
164
src/views/corpusManger/corpus/components/CorpusList.vue
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<el-card shadow="never" class="corpus-list">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="list-toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<el-input
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索文本内容、语料编号或说话人"
|
||||||
|
size="default"
|
||||||
|
style="width: 300px;"
|
||||||
|
clearable
|
||||||
|
prefix-icon="Search"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" size="default">+新建语料</el-button>
|
||||||
|
<el-button size="default" plain>批量导入</el-button>
|
||||||
|
<el-button size="default" plain>导出数据</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<el-tag
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
:type="activeTab === tab.key ? '' : 'info'"
|
||||||
|
:effect="activeTab === tab.key ? 'dark' : 'plain'"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
@click="activeTab = tab.key"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
<span class="tab-count">{{ tab.count }}</span>
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table :data="filteredData" stripe style="width:100%;" max-height="400">
|
||||||
|
<el-table-column prop="id" label="语料编号" width="180" />
|
||||||
|
<el-table-column prop="text" label="文本内容" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="category" label="功能类别" width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="lang" label="语种" width="80" />
|
||||||
|
<el-table-column prop="dialect" label="方言" width="80" />
|
||||||
|
<el-table-column prop="emotion" label="情绪" width="70" />
|
||||||
|
<el-table-column prop="voice" label="音色" width="100" />
|
||||||
|
<el-table-column prop="status" label="审核状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusType(row.status)" size="small" effect="plain">
|
||||||
|
{{ row.status }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" width="160" />
|
||||||
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" text type="primary" @click="handleDetail(row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<span class="text-muted text-sm">共 {{ total }} 条</span>
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
:page-size="10"
|
||||||
|
v-model:current-page="page"
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['view-detail'])
|
||||||
|
|
||||||
|
const searchText = ref('')
|
||||||
|
const activeTab = ref('all')
|
||||||
|
const page = ref(1)
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'all', label: '全部语料', count: '1,245,678' },
|
||||||
|
{ key: 'pending', label: '待审核', count: '45,678' },
|
||||||
|
{ key: 'published', label: '已发布', count: '987,654' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const corpusData = ref([
|
||||||
|
{ id: 'CORP_20240517_0001', text: '打开天窗', category: '控制指令-车窗控制', lang: '中文', dialect: '普通话', emotion: '中性', voice: '男声', status: '已通过', updateTime: '2024-05-17 10:21:33' },
|
||||||
|
{ id: 'CORP_20240517_0002', text: '导航到最近的充电站', category: '导航交互-路线规划', lang: '中文', dialect: '普通话', emotion: '中性', voice: '女声', status: '已通过', updateTime: '2024-05-17 10:19:48' },
|
||||||
|
{ id: 'CORP_20240517_0003', text: '播放周杰伦的歌', category: '媒体娱乐-音乐播放', lang: '中文', dialect: '普通话', emotion: '愉悦', voice: '女声', status: '已通过', updateTime: '2024-05-17 10:18:02' },
|
||||||
|
{ id: 'CORP_20240517_0004', text: '把空调调到24度', category: '控制指令-空调控制', lang: '中文', dialect: '普通话', emotion: '中性', voice: '男声', status: '待审核', updateTime: '2024-05-17 10:15:26' },
|
||||||
|
{ id: 'CORP_20240517_0005', text: '给张三打电话', category: '电话消息-拨打电话', lang: '中文', dialect: '普通话', emotion: '中性', voice: '男声', status: '已通过', updateTime: '2024-05-17 10:12:11' },
|
||||||
|
{ id: 'CORP_20240517_0006', text: '今天天气怎么样', category: '场景对话-日常对话', lang: '中文', dialect: '普通话', emotion: '中性', voice: '女声', status: '已通过', updateTime: '2024-05-17 10:10:05' },
|
||||||
|
{ id: 'CORP_20240517_0007', text: '我有点冷', category: '场景对话-出行场景', lang: '中文', dialect: '普通话', emotion: '不满', voice: '女声', status: '待审核', updateTime: '2024-05-17 10:08:47' },
|
||||||
|
{ id: 'CORP_20240517_0008', text: '关闭所有车窗', category: '控制指令-车窗控制', lang: '中文', dialect: '普通话', emotion: '中性', voice: '男声', status: '已通过', updateTime: '2024-05-17 10:06:20' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredData = computed(() => {
|
||||||
|
let list = corpusData.value
|
||||||
|
if (activeTab.value === 'pending') {
|
||||||
|
list = list.filter(item => item.status === '待审核')
|
||||||
|
} else if (activeTab.value === 'published') {
|
||||||
|
list = list.filter(item => item.status === '已通过')
|
||||||
|
}
|
||||||
|
if (searchText.value) {
|
||||||
|
const s = searchText.value.toLowerCase()
|
||||||
|
list = list.filter(item =>
|
||||||
|
item.id.toLowerCase().includes(s) ||
|
||||||
|
item.text.includes(s) ||
|
||||||
|
item.voice.includes(s)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
const total = computed(() => filteredData.value.length)
|
||||||
|
|
||||||
|
const statusType = (status) => {
|
||||||
|
if (status === '已通过') return 'success'
|
||||||
|
if (status === '待审核') return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetail = (row) => {
|
||||||
|
emit('view-detail', row)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.corpus-list {
|
||||||
|
.list-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
.toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.tab-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.text-muted { color: #94a3b8; }
|
||||||
|
.text-sm { font-size: 13px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
145
src/views/corpusManger/corpus/components/StatCards.vue
Normal file
145
src/views/corpusManger/corpus/components/StatCards.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row-cards">
|
||||||
|
<div class="card" v-for="item in stats" :key="item.label" shadow="never">
|
||||||
|
<div class="flex-between">
|
||||||
|
<div class="icon-container" :class="item.iconName">
|
||||||
|
<SvgIcon :name="item.iconName" :size="28" color="#fff" />
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<span class="stat-label">{{ item.label }}</span>
|
||||||
|
<div class="stat-number mt-2">{{ item.value }}</div>
|
||||||
|
<span v-if="item.trend" class="stat-trend" :class="item.trendClass">
|
||||||
|
<span>较昨日</span>{{ item.trend }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from "@/components/SvgIcon";
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
iconName: 'task',
|
||||||
|
label: '语料总量(条)',
|
||||||
|
value: '856,420',
|
||||||
|
trend: '+12.4%',
|
||||||
|
trendClass: 'up'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'add',
|
||||||
|
label: '核心功能分类(个)',
|
||||||
|
value: '12,680',
|
||||||
|
trend: '+8.7%',
|
||||||
|
trendClass: 'up',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'language',
|
||||||
|
label: '多语种数量(种)',
|
||||||
|
value: '12',
|
||||||
|
trend: '—',
|
||||||
|
trendClass: 'neutral'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'consuming',
|
||||||
|
label: '今日新增语料(条)',
|
||||||
|
value: '36',
|
||||||
|
trend: '+3.2%',
|
||||||
|
trendClass: 'up'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.row-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px 32px;
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task {
|
||||||
|
background: #196afd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
background: #16cb8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language {
|
||||||
|
background: #735df6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consuming {
|
||||||
|
background: #fd9131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timbre {
|
||||||
|
background: #2f7bf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-trend {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 6px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.up {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.down {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.neutral {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
src/views/corpusManger/corpus/index.vue
Normal file
59
src/views/corpusManger/corpus/index.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<StatCards />
|
||||||
|
|
||||||
|
<!-- 主体:目录树 + 列表 + 详情 -->
|
||||||
|
<div class="main-layout">
|
||||||
|
<div class="left-panel">
|
||||||
|
<CategoryTree @select-category="onSelectCategory" />
|
||||||
|
</div>
|
||||||
|
<div class="right-panel">
|
||||||
|
<CorpusList @view-detail="onViewDetail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import StatCards from './components/StatCards.vue'
|
||||||
|
import CategoryTree from './components/CategoryTree.vue'
|
||||||
|
import CorpusList from './components/CorpusList.vue'
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.left-panel {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
.right-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.detail-wrapper {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.left-panel {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,353 @@
|
|||||||
|
<template>
|
||||||
|
<div class="audio-player">
|
||||||
|
<!-- 隐藏的 audio 元素 -->
|
||||||
|
<audio ref="audioRef" :src="src" preload="auto" @loadedmetadata="onLoadedMetadata" @timeupdate="onTimeUpdate"
|
||||||
|
@ended="onEnded" @canplay="onCanPlay"></audio>
|
||||||
|
|
||||||
|
<!-- 播放器主体 -->
|
||||||
|
<div class="player-main">
|
||||||
|
<!-- 播放/暂停按钮 -->
|
||||||
|
<SvgIcon class="btn" v-if="!isPlaying" name="start" :size="32" color="#136cfc" @click="togglePlay" />
|
||||||
|
<SvgIcon class="btn" v-if="isPlaying" name="pause" :size="32" color="#d33257" @click="togglePlay" />
|
||||||
|
|
||||||
|
<!-- 当前时间 -->
|
||||||
|
<span class="time-text">{{ formatTime(currentTime) }}</span>
|
||||||
|
|
||||||
|
<!-- 自定义进度条 -->
|
||||||
|
<div class="progress-wrapper">
|
||||||
|
<!-- 缓冲条 -->
|
||||||
|
<div class="buffered-bar" :style="{ width: buffered + '%' }"></div>
|
||||||
|
<!-- 已播放进度条 -->
|
||||||
|
<div class="progress-bar" :style="{ width: progress + '%' }">
|
||||||
|
<div class="progress-thumb" :class="{ active: isDragging }"></div>
|
||||||
|
</div>
|
||||||
|
<!-- 透明的交互层 -->
|
||||||
|
<div class="progress-hit" ref="progressRef" @mousedown="onProgressMouseDown"
|
||||||
|
@mousemove="onProgressHover" @mouseleave="hoverTime = -1"></div>
|
||||||
|
<!-- 悬浮时间提示 -->
|
||||||
|
<div v-if="hoverTime >= 0 && !isDragging" class="hover-tooltip" :style="{ left: hoverPos + 'px' }">
|
||||||
|
{{ formatTime(hoverTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 总时长 -->
|
||||||
|
<span class="time-text">{{ formatTime(duration) }}</span>
|
||||||
|
|
||||||
|
<!-- 音量控制 -->
|
||||||
|
<div class="volume-wrapper">
|
||||||
|
<el-button :icon="volumeIcon" text @click="toggleMute" />
|
||||||
|
<el-slider v-model="volume" :min="0" :max="100" :show-tooltip="false" size="small" class="volume-slider"
|
||||||
|
@input="onVolumeChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onBeforeUnmount, watch } from 'vue'
|
||||||
|
import SvgIcon from "@/components/SvgIcon";
|
||||||
|
import { Mute, Microphone } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['audioStatus'])
|
||||||
|
|
||||||
|
// --- 状态 ---
|
||||||
|
const audioRef = ref(null)
|
||||||
|
const progressRef = ref(null)
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const buffered = ref(0)
|
||||||
|
const volume = ref(80)
|
||||||
|
const lastVolume = ref(80)
|
||||||
|
const hoverTime = ref(-1)
|
||||||
|
const hoverPos = ref(0)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
|
||||||
|
// --- 计算属性 ---
|
||||||
|
const progress = computed(() => {
|
||||||
|
if (!duration.value) return 0
|
||||||
|
return (currentTime.value / duration.value) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const volumeIcon = computed(() => {
|
||||||
|
return volume.value === 0 ? Mute : Microphone
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- audio 事件 ---
|
||||||
|
const onLoadedMetadata = () => {
|
||||||
|
duration.value = audioRef.value.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCanPlay = () => {
|
||||||
|
duration.value = audioRef.value.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTimeUpdate = () => {
|
||||||
|
if (!isDragging.value) {
|
||||||
|
currentTime.value = audioRef.value.currentTime
|
||||||
|
updateBuffered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEnded = () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
currentTime.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBuffered = () => {
|
||||||
|
const audio = audioRef.value
|
||||||
|
if (audio.buffered.length > 0) {
|
||||||
|
buffered.value = (audio.buffered.end(audio.buffered.length - 1) / duration.value) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 播放控制 ---
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (!audioRef.value && !props.url) return
|
||||||
|
if (isPlaying.value) {
|
||||||
|
audioRef.value.pause()
|
||||||
|
} else {
|
||||||
|
audioRef.value.play()
|
||||||
|
}
|
||||||
|
isPlaying.value = !isPlaying.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 进度条交互 ---
|
||||||
|
const getTimeFromEvent = (e) => {
|
||||||
|
const rect = progressRef.value.getBoundingClientRect()
|
||||||
|
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||||
|
return ratio * duration.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekTo = (time) => {
|
||||||
|
if (!audioRef.value || !duration.value) return
|
||||||
|
audioRef.value.currentTime = Math.max(0, Math.min(time, duration.value))
|
||||||
|
currentTime.value = audioRef.value.currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProgressMouseDown = (e) => {
|
||||||
|
isDragging.value = true
|
||||||
|
seekTo(getTimeFromEvent(e))
|
||||||
|
|
||||||
|
const onMove = (ev) => {
|
||||||
|
seekTo(getTimeFromEvent(ev))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUp = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
document.removeEventListener('mousemove', onMove)
|
||||||
|
document.removeEventListener('mouseup', onUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMove)
|
||||||
|
document.addEventListener('mouseup', onUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProgressHover = (e) => {
|
||||||
|
if (isDragging.value) return
|
||||||
|
const rect = progressRef.value.getBoundingClientRect()
|
||||||
|
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||||
|
hoverTime.value = ratio * duration.value
|
||||||
|
hoverPos.value = e.clientX - rect.left
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 音量控制 ---
|
||||||
|
const onVolumeChange = (val) => {
|
||||||
|
if (!audioRef.value) return
|
||||||
|
audioRef.value.volume = val / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMute = () => {
|
||||||
|
if (volume.value > 0) {
|
||||||
|
lastVolume.value = volume.value
|
||||||
|
volume.value = 0
|
||||||
|
} else {
|
||||||
|
volume.value = lastVolume.value
|
||||||
|
}
|
||||||
|
audioRef.value.volume = volume.value / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 初始化音量 ---
|
||||||
|
const initVolume = () => {
|
||||||
|
if (audioRef.value) {
|
||||||
|
audioRef.value.volume = volume.value / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 工具函数 ---
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
if (!seconds || isNaN(seconds)) return '00:00'
|
||||||
|
const m = Math.floor(seconds / 60)
|
||||||
|
const s = Math.floor(seconds % 60)
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 audio 元素加载完成后设置音量
|
||||||
|
const observer = setInterval(() => {
|
||||||
|
if (audioRef.value) {
|
||||||
|
initVolume()
|
||||||
|
clearInterval(observer)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
watch(() => isPlaying.value,
|
||||||
|
() => {
|
||||||
|
emits('audioStatus', isPlaying.value)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(observer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.audio-player {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border: 1px solid #afafaf;
|
||||||
|
|
||||||
|
.player-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
|
||||||
|
/* 时间文字 */
|
||||||
|
.time-text {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
width: 42px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'DM Mono', 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条容器 */
|
||||||
|
.progress-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: #e5e5e5;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.buffered-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.05s linear;
|
||||||
|
|
||||||
|
.progress-thumb {
|
||||||
|
position: absolute;
|
||||||
|
right: -7px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%) scale(0);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 8px rgba(59, 130, 246, 0.6);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 透明交互层 */
|
||||||
|
.progress-hit {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -8px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 时间提示 */
|
||||||
|
.hover-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
top: -32px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 时显示圆点 */
|
||||||
|
.progress-wrapper:hover .progress-thumb,
|
||||||
|
.progress-thumb.active {
|
||||||
|
transform: translateY(-50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 音量区域 */
|
||||||
|
.volume-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-wrapper .el-button {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.volume-slider .el-slider__runway) {
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.volume-slider .el-slider__bar) {
|
||||||
|
height: 4px;
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.volume-slider .el-slider__button-wrapper) {
|
||||||
|
top: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.volume-slider .el-slider__button) {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条 hover 加粗效果 */
|
||||||
|
.progress-wrapper:hover {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-workspace">
|
||||||
|
<div class="workspace-container">
|
||||||
|
<!-- 左栏:文本与参数配置 -->
|
||||||
|
<div class="left">
|
||||||
|
<div class="title">
|
||||||
|
文本与参数配置
|
||||||
|
</div>
|
||||||
|
<el-form ref="formRef" :rules="rules" :model="form" label-position="top" label-width="auto">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="" prop="text">
|
||||||
|
<el-input type="textarea" :rows="3" v-model="form.text" placeholder="请输入待合成文本" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="音色" prop="voice">
|
||||||
|
<el-select v-model="form.voice" placeholder="音色">
|
||||||
|
<el-option v-for="item in voiceOption" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="音量">
|
||||||
|
<el-slider v-model="form.volume" :min="1" :max="100" :step="1" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="语速" prop="speed">
|
||||||
|
<el-select v-model="form.speed" placeholder="语速">
|
||||||
|
<el-slider v-model="form.speed" :min="1" :max="100" :step="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="音高">
|
||||||
|
<el-slider v-model="form.pitch" :min="1" :max="100" :step="1" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="采样率">
|
||||||
|
<el-select v-model="form.auf" placeholder="采样率">
|
||||||
|
<el-option label="8kHz" value="8" />
|
||||||
|
<el-option label="16kHz" value="16" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="输出格式">
|
||||||
|
<el-select v-model="form.outputFormat" placeholder="输出格式">
|
||||||
|
<el-option label="WAV" value="wav" />
|
||||||
|
<el-option label="男声-沉稳" value="male-calm" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="margin-top: 20px;" :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button size="small" type="primary" :icon="VideoPlay" class="btn"
|
||||||
|
@click="synthesis">开始合成</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button size="small" type="primary" plain :icon="Delete" class="btn"
|
||||||
|
@click="cancel">清空</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<!-- 右栏:语音预览与波形 -->
|
||||||
|
<div class="right">
|
||||||
|
<div class="title">语音预览与波形</div>
|
||||||
|
<div class="wave-container">
|
||||||
|
<div class="wave">
|
||||||
|
<div v-for="(h, index) in waveform" :key="index" :style="{
|
||||||
|
width: '4px',
|
||||||
|
height: h + '%',
|
||||||
|
background: '#3b82f6',
|
||||||
|
borderRadius: '2px',
|
||||||
|
opacity: 0.6 + (h / 100) * 0.4
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="audio-container">
|
||||||
|
<AudioPlayer :src="audioUrl" @audioStatus="audioStatus" />
|
||||||
|
</div>
|
||||||
|
<div class="card-container">
|
||||||
|
<div v-for="item in cardList" class="card">
|
||||||
|
<div class="title">{{ item.title }}</div>
|
||||||
|
<div class="value">{{ item.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-row style="margin-top: 10px;" :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button type="primary" plain class="btn" @click="regenerate">重新生成</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button type="primary" class="btn" >保存</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { Delete, VideoPlay } from '@element-plus/icons-vue'
|
||||||
|
import { taskApi, regenerateApi } from '@/api/corpusManger/speech_synthesis'
|
||||||
|
import AudioPlayer from './AudioPlayer.vue';
|
||||||
|
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
text: '',
|
||||||
|
voice: 'xiaoyan',
|
||||||
|
speed: 50,
|
||||||
|
volume: 50,
|
||||||
|
pitch: 50,
|
||||||
|
auf: '16',
|
||||||
|
outputFormat: 'wav'
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
text: [{ required: true, message: '请输入合成文本', trigger: 'blur' }],
|
||||||
|
voice: [{ required: true, message: '请输入音色', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const langOption = ref([
|
||||||
|
{
|
||||||
|
label: '中文(普通话)',
|
||||||
|
value: '普通话'
|
||||||
|
}, {
|
||||||
|
label: '英文',
|
||||||
|
value: '英文'
|
||||||
|
}, {
|
||||||
|
label: '粤语',
|
||||||
|
value: '粤语'
|
||||||
|
}, {
|
||||||
|
label: '四川话',
|
||||||
|
value: '四川话'
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const dialectOption = ref([
|
||||||
|
{
|
||||||
|
label: '东北话',
|
||||||
|
value: '东北话'
|
||||||
|
}, {
|
||||||
|
label: '四川话',
|
||||||
|
value: '四川话'
|
||||||
|
}, {
|
||||||
|
label: '广东话',
|
||||||
|
value: '广东话'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const voiceOption = ref([
|
||||||
|
{
|
||||||
|
label: '女声',
|
||||||
|
value: 'xiaoyan'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const emotionOption = ref([
|
||||||
|
{
|
||||||
|
label: '开心',
|
||||||
|
value: '开心'
|
||||||
|
}, {
|
||||||
|
label: '严肃',
|
||||||
|
value: '严肃'
|
||||||
|
}, {
|
||||||
|
label: '温和',
|
||||||
|
value: '温和'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const currentSynthesis = ref({
|
||||||
|
id: null,
|
||||||
|
fileSize: null,
|
||||||
|
outputFormat: null,
|
||||||
|
text: '',
|
||||||
|
audioFilePath: ''
|
||||||
|
})
|
||||||
|
const synthesis = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const result = await formRef.value.validate()
|
||||||
|
if (result) {
|
||||||
|
const res = await taskApi(form.value)
|
||||||
|
console.log('res', res)
|
||||||
|
if (res.code === 200) {
|
||||||
|
currentSynthesis.value = {
|
||||||
|
...res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const regenerate = async () => {
|
||||||
|
const res = await regenerateApi(currentSynthesis.value.id)
|
||||||
|
console.log('res', res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
formRef.value.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardList = ref([
|
||||||
|
{
|
||||||
|
title: '预计时长',
|
||||||
|
value: '00:06'
|
||||||
|
}, {
|
||||||
|
title: '字符数',
|
||||||
|
value: '21'
|
||||||
|
}, {
|
||||||
|
title: '采样率',
|
||||||
|
value: '48kHz'
|
||||||
|
}, {
|
||||||
|
title: '文件大小',
|
||||||
|
value: '256KB'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 波形数据
|
||||||
|
const waveform = ref(Array.from({ length: 80 }, () => Math.floor(Math.random() * 60 + 20)))
|
||||||
|
const timer = ref(null)
|
||||||
|
|
||||||
|
// 模拟波形动态变化
|
||||||
|
const updateWaveform = () => {
|
||||||
|
waveform.value = Array.from({ length: 80 }, () => Math.floor(Math.random() * 60 + 20))
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioUrl = ref('https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/music/audio.mp3')
|
||||||
|
|
||||||
|
const audioStatus = (isPlaying) => {
|
||||||
|
if (isPlaying) {
|
||||||
|
timer.value = setInterval(updateWaveform, 1000)
|
||||||
|
} else {
|
||||||
|
if (timer.value) clearInterval(timer.value)
|
||||||
|
cancelAnimationFrame(animationFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer.value) clearInterval(timer.value)
|
||||||
|
cancelAnimationFrame(animationFrame)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.main-workspace {
|
||||||
|
.workspace-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex: 1;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-container {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 140px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.wave {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 80px;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid #d3d3d3;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
152
src/views/corpusManger/speech_synthesis/components/StatCards.vue
Normal file
152
src/views/corpusManger/speech_synthesis/components/StatCards.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row-cards">
|
||||||
|
<div class="card" v-for="item in stats" :key="item.label" shadow="never">
|
||||||
|
<div class="flex-between">
|
||||||
|
<div class="icon-container" :class="item.iconName">
|
||||||
|
<SvgIcon :name="item.iconName" :size="28" color="#fff" />
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<span class="stat-label">{{ item.label }}</span>
|
||||||
|
<div class="stat-number mt-2">{{ item.value }}</div>
|
||||||
|
<span v-if="item.trend" class="stat-trend" :class="item.trendClass">
|
||||||
|
<span>较昨日</span>{{ item.trend }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from "@/components/SvgIcon";
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
iconName: 'task',
|
||||||
|
label: '合成任务总数(条)',
|
||||||
|
value: '856,420',
|
||||||
|
trend: '+12.4%',
|
||||||
|
trendClass: 'up'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'add',
|
||||||
|
label: '今日新增合成(条)',
|
||||||
|
value: '12,680',
|
||||||
|
trend: '+8.7%',
|
||||||
|
trendClass: 'up',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'language',
|
||||||
|
label: '支持语种(种)',
|
||||||
|
value: '12',
|
||||||
|
trend: '—',
|
||||||
|
trendClass: 'neutral'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'consuming',
|
||||||
|
label: '可用音色(个)',
|
||||||
|
value: '36',
|
||||||
|
trend: '+3.2%',
|
||||||
|
trendClass: 'up'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'timbre',
|
||||||
|
label: '平均生成时延(ms)',
|
||||||
|
value: '428',
|
||||||
|
trend: '-5.6%',
|
||||||
|
trendClass: 'down'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.row-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px 32px;
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task {
|
||||||
|
background: #196afd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
background: #16cb8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language {
|
||||||
|
background: #735df6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consuming {
|
||||||
|
background: #fd9131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timbre {
|
||||||
|
background: #2f7bf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-trend {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 6px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.up {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.down {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.neutral {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
180
src/views/corpusManger/speech_synthesis/components/TaskList.vue
Normal file
180
src/views/corpusManger/speech_synthesis/components/TaskList.vue
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div class="flex-between mb-3">
|
||||||
|
<span class="stat-label">合成任务列表</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-between mb-3">
|
||||||
|
<div class="flex-center gap-2">
|
||||||
|
<el-tag v-for="tab in tabs" :key="tab.key" :type="activeTab === tab.key ? '' : 'info'"
|
||||||
|
:effect="activeTab === tab.key ? 'dark' : 'plain'" style="cursor:pointer;"
|
||||||
|
@click="changeTab(tab.key)">
|
||||||
|
{{ tab.label }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-table :data="tasks" style="width:100%;" stripe>
|
||||||
|
<el-table-column prop="taskCode" label="任务编号" />
|
||||||
|
<el-table-column prop="text" label="文本内容" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="voice" label="音色" />
|
||||||
|
<el-table-column prop="status" label="状态">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusType(row.status)" size="small" effect="plain">
|
||||||
|
{{ row.statusName }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="audioDuration" label="时间">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatTime(scope.row.audioDuration) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" class-name="small-padding fixed-width" label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link @click="play(scope.rows.audioFilePath)" type="primary" icon="VideoPlay"></el-button>
|
||||||
|
<el-button icon="Delete" link type="primary" @click="handleDelete(scope.row.id)"></el-button>
|
||||||
|
<el-button icon="Download" link type="primary" @click="handleDownload(scope.row)"></el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="flex-between mt-3">
|
||||||
|
<span class="text-muted text-sm">共 {{ totalTasks }} 条</span>
|
||||||
|
<el-pagination background layout="prev, pager, next, jumper" :total="totalTasks" :page-size="10"
|
||||||
|
v-model:current-page="page" small />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { taskListApi, deleteTask, downloadWavApi } from '@/api/corpusManger/speech_synthesis'
|
||||||
|
import { usePlayAudio } from '@/hooks/usePlayAudio'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import Download from '@/plugins/download'
|
||||||
|
|
||||||
|
const search = ref('')
|
||||||
|
const activeTab = ref('')
|
||||||
|
const page = ref(1)
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: '', label: '全部任务' },
|
||||||
|
{ key: '1', label: '合成中' },
|
||||||
|
{ key: '2', label: '已合成' },
|
||||||
|
{ key: '3', label: '失败' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const tasks = ref([])
|
||||||
|
|
||||||
|
const totalTasks = computed(() => tasks.value.length)
|
||||||
|
|
||||||
|
const changeTab = (key) => {
|
||||||
|
activeTab.value = key
|
||||||
|
getTaskList(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap = { '2': 'success', '1': 'warning', '0': 'info', '3': 'danger' }
|
||||||
|
|
||||||
|
const statusType = (status) => {
|
||||||
|
return statusMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
if (!seconds || isNaN(seconds)) return '00:00'
|
||||||
|
const m = Math.floor(seconds / 60)
|
||||||
|
const s = Math.floor(seconds % 60)
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTaskList = async (status = null) => {
|
||||||
|
const obj = {
|
||||||
|
pageNum: page.value,
|
||||||
|
pageSize: 10,
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
obj.status = status
|
||||||
|
}
|
||||||
|
const res = await taskListApi(obj)
|
||||||
|
if (res.code == 0) {
|
||||||
|
tasks.value = res.rows
|
||||||
|
totalTasks.value = res.total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const play = (url) => {
|
||||||
|
const { playAudio } = usePlayAudio()
|
||||||
|
playAudio(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
ElMessageBox.confirm('是否删除该任务', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
const res = await deleteTask(id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
getTaskList()
|
||||||
|
ElMessage.success(res.msg)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg)
|
||||||
|
}
|
||||||
|
}).catch(() => { });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = async (row) => {
|
||||||
|
const res = await downloadWavApi(row.id)
|
||||||
|
const blob = new Blob([res], { type: `audio/${row.outputFormat}` })
|
||||||
|
const blobUrl = URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = blobUrl
|
||||||
|
a.download = `${row.id}.${row.outputFormat}`
|
||||||
|
a.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getTaskList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-3 {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
37
src/views/corpusManger/speech_synthesis/index.vue
Normal file
37
src/views/corpusManger/speech_synthesis/index.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="statCards">
|
||||||
|
<StatCards />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主工作区 -->
|
||||||
|
<div class="mainWorkspace">
|
||||||
|
<MainWorkspace />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务列表 -->
|
||||||
|
<TaskList />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import StatCards from './components/StatCards.vue'
|
||||||
|
import MainWorkspace from './components/MainWorkspace.vue'
|
||||||
|
import TaskList from './components/TaskList.vue'
|
||||||
|
</script>
|
||||||
|
<script setup></script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.statCards {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainWorkspace {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user