优化
This commit is contained in:
@@ -54,17 +54,27 @@ async function handleSave() {
|
||||
|
||||
savingPrompt.value = true
|
||||
try {
|
||||
// 确保 content 字段有值
|
||||
const content = savePromptForm.value.content?.trim() || ''
|
||||
if (!content) {
|
||||
message.error('提示词内容不能为空')
|
||||
savingPrompt.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
userId: userId,
|
||||
name: savePromptForm.value.name.trim(),
|
||||
content: savePromptForm.value.content.trim(),
|
||||
category: savePromptForm.value.category.trim() || null,
|
||||
content: content, // 确保 content 有值
|
||||
category: savePromptForm.value.category?.trim() || null,
|
||||
isPublic: false,
|
||||
sort: 0,
|
||||
useCount: 0,
|
||||
status: 1,
|
||||
}
|
||||
|
||||
console.log('[SavePromptModal] 发送请求参数:', payload)
|
||||
|
||||
const response = await UserPromptApi.createUserPrompt(payload)
|
||||
|
||||
if (response && (response.code === 0 || response.code === 200)) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,10 @@
|
||||
<div class="voice-copy-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">配音管理</h1>
|
||||
<h1>配音管理</h1>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>新建配音</span>
|
||||
<PlusOutlined />
|
||||
新建配音
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -18,12 +16,9 @@
|
||||
v-model:value="searchParams.name"
|
||||
placeholder="搜索配音名称"
|
||||
style="width: 250px"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
<SearchOutlined />
|
||||
</a-input>
|
||||
<a-button type="primary" @click="handleSearch">查询</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
@@ -41,43 +36,37 @@
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="voice-name">{{ record.name || '未命名' }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'transcription'">
|
||||
<div class="transcription-text">{{ formatTranscription(record.transcription) }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createTime'">
|
||||
<span>{{ formatDateTime(record.createTime) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'fileUrl'">
|
||||
<a-button type="link" size="small" @click="handlePlayAudio(record)">
|
||||
<template #icon>
|
||||
<PlayCircleOutlined />
|
||||
</template>
|
||||
播放
|
||||
<div v-if="column.key === 'name'" class="voice-name">
|
||||
{{ record.name || '未命名' }}
|
||||
</div>
|
||||
<div v-else-if="column.key === 'transcription'" class="transcription-text">
|
||||
{{ formatTranscription(record.transcription) }}
|
||||
</div>
|
||||
<span v-else-if="column.key === 'createTime'">
|
||||
{{ formatDateTime(record.createTime) }}
|
||||
</span>
|
||||
<a-button v-else-if="column.key === 'fileUrl'" type="link" size="small" @click="handlePlayAudio(record)">
|
||||
<PlayCircleOutlined />
|
||||
播放
|
||||
</a-button>
|
||||
<a-space v-else-if="column.key === 'actions'">
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:loading="transcribingId === record.id"
|
||||
:disabled="!!record.transcription"
|
||||
@click="handleTranscribe(record)"
|
||||
>
|
||||
{{ record.transcription ? '已识别' : '识别' }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:loading="transcribingId === record.id"
|
||||
:disabled="!!record.transcription"
|
||||
@click="handleTranscribe(record)"
|
||||
>
|
||||
{{ record.transcription ? '已识别' : '识别' }}
|
||||
</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑表单 Modal -->
|
||||
<!-- 表单 Modal -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="isCreateMode ? '新建配音' : '编辑配音'"
|
||||
@@ -86,12 +75,7 @@
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<a-form-item label="配音名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入配音名称" />
|
||||
</a-form-item>
|
||||
@@ -106,37 +90,29 @@
|
||||
v-model:file-list="fileList"
|
||||
:custom-request="handleCustomUpload"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:show-upload-list="true"
|
||||
:max-count="1"
|
||||
accept="audio/*,.mp3,.wav,.aac,.m4a,.flac,.ogg"
|
||||
@remove="handleRemoveFile"
|
||||
@change="handleFileListChange"
|
||||
>
|
||||
<a-button type="primary" :loading="uploading">
|
||||
<template #icon>
|
||||
<UploadOutlined v-if="!uploading" />
|
||||
</template>
|
||||
<UploadOutlined v-if="!uploading" />
|
||||
{{ uploading ? '上传中...' : (fileList.length > 0 ? '重新上传' : '上传音频文件') }}
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div class="upload-hint">支持格式:MP3、WAV、AAC、M4A、FLAC、OGG,单个文件不超过 100MB</div>
|
||||
<div class="upload-hint">
|
||||
支持格式:MP3、WAV、AAC、M4A、FLAC、OGG,单个文件不超过 50MB<br>
|
||||
<span class="hint-text">🎤 配音建议:使用 30 秒 - 2 分钟的短配音效果更佳</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="note">
|
||||
<a-textarea
|
||||
v-model:value="formData.note"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
<a-textarea v-model="formData.note" :rows="3" placeholder="请输入备注信息" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="!isCreateMode"
|
||||
label="识别内容"
|
||||
name="transcription"
|
||||
>
|
||||
<a-form-item v-if="!isCreateMode" label="识别内容" name="transcription">
|
||||
<a-textarea
|
||||
v-model:value="formData.transcription"
|
||||
v-model="formData.transcription"
|
||||
:rows="4"
|
||||
placeholder="识别内容,支持手动修改"
|
||||
/>
|
||||
@@ -144,29 +120,23 @@
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 音频播放器 -->
|
||||
<audio ref="audioPlayer" style="display: none" controls />
|
||||
<audio ref="audioPlayer" style="display: none" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
UploadOutlined,
|
||||
PlayCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { PlusOutlined, SearchOutlined, UploadOutlined, PlayCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { VoiceService } from '@/api/voice'
|
||||
import { MaterialService } from '@/api/material'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// ========== 常量定义 ==========
|
||||
// ========== 常量 ==========
|
||||
const POLLING_CONFIG = {
|
||||
interval: 10000, // 轮询间隔:10秒
|
||||
maxCount: 30, // 最大轮询次数:30次(5分钟)
|
||||
transcriptionMaxLength: 50 // 识别内容最大显示长度
|
||||
interval: 10000,
|
||||
maxCount: 30,
|
||||
transcriptionMaxLength: 50
|
||||
}
|
||||
|
||||
const DEFAULT_FORM_DATA = {
|
||||
@@ -254,18 +224,15 @@ const fillFormData = (data) => {
|
||||
const loadVoiceList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
const res = await VoiceService.getPage({
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
name: searchParams.name || undefined
|
||||
}
|
||||
const res = await VoiceService.getPage(params)
|
||||
if (res.code === 0) {
|
||||
voiceList.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else {
|
||||
message.error(res.msg || '加载失败')
|
||||
}
|
||||
})
|
||||
if (res.code !== 0) return message.error(res.msg || '加载失败')
|
||||
|
||||
voiceList.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载配音列表失败:', error)
|
||||
message.error('加载失败,请稍后重试')
|
||||
@@ -303,14 +270,10 @@ const handleEdit = async (record) => {
|
||||
formMode.value = 'edit'
|
||||
try {
|
||||
const res = await VoiceService.get(record.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
fillFormData(res.data)
|
||||
} else {
|
||||
fillFormData(record) // 获取失败时使用列表数据
|
||||
}
|
||||
fillFormData(res.code === 0 && res.data ? res.data : record)
|
||||
} catch (error) {
|
||||
console.error('获取配音详情失败:', error)
|
||||
fillFormData(record) // 异常时使用列表数据
|
||||
fillFormData(record)
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
@@ -325,12 +288,10 @@ const handleDelete = (record) => {
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await VoiceService.delete(record.id)
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功')
|
||||
loadVoiceList()
|
||||
} else {
|
||||
message.error(res.msg || '删除失败')
|
||||
}
|
||||
if (res.code !== 0) return message.error(res.msg || '删除失败')
|
||||
|
||||
message.success('删除成功')
|
||||
loadVoiceList()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败,请稍后重试')
|
||||
@@ -344,13 +305,14 @@ const handleTranscribe = async (record) => {
|
||||
transcribingId.value = record.id
|
||||
try {
|
||||
const res = await VoiceService.transcribe(record.id)
|
||||
if (res.code === 0) {
|
||||
message.success('识别任务已提交,正在识别中...')
|
||||
startPollingTranscription(record.id)
|
||||
} else {
|
||||
if (res.code !== 0) {
|
||||
message.error(res.msg || '识别失败')
|
||||
transcribingId.value = null
|
||||
return
|
||||
}
|
||||
|
||||
message.success('识别任务已提交,正在识别中...')
|
||||
startPollingTranscription(record.id)
|
||||
} catch (error) {
|
||||
console.error('识别失败:', error)
|
||||
message.error('识别失败,请稍后重试')
|
||||
@@ -368,11 +330,11 @@ const stopPolling = () => {
|
||||
|
||||
const startPollingTranscription = (voiceId) => {
|
||||
stopPolling()
|
||||
|
||||
|
||||
let pollCount = 0
|
||||
pollingTimer = setInterval(async () => {
|
||||
pollCount++
|
||||
|
||||
|
||||
try {
|
||||
const res = await VoiceService.get(voiceId)
|
||||
if (res.code === 0 && res.data?.transcription) {
|
||||
@@ -381,7 +343,7 @@ const startPollingTranscription = (voiceId) => {
|
||||
loadVoiceList()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (pollCount >= POLLING_CONFIG.maxCount) {
|
||||
stopPolling()
|
||||
message.warning('识别超时,请稍后手动刷新查看结果')
|
||||
@@ -389,9 +351,7 @@ const startPollingTranscription = (voiceId) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询识别结果失败:', error)
|
||||
if (pollCount >= POLLING_CONFIG.maxCount) {
|
||||
stopPolling()
|
||||
}
|
||||
if (pollCount >= POLLING_CONFIG.maxCount) stopPolling()
|
||||
}
|
||||
}, POLLING_CONFIG.interval)
|
||||
}
|
||||
@@ -408,79 +368,53 @@ const handlePlayAudio = (record) => {
|
||||
|
||||
// ========== 文件上传 ==========
|
||||
const handleBeforeUpload = (file) => {
|
||||
// 检查文件大小(100MB)
|
||||
const MAX_FILE_SIZE = 100 * 1024 * 1024
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
message.error('文件大小不能超过 100MB')
|
||||
message.error('文件大小不能超过 50MB')
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
const validTypes = ['audio/mpeg', 'audio/wav', 'audio/wave', 'audio/x-wav', 'audio/aac', 'audio/mp4', 'audio/flac', 'audio/ogg']
|
||||
const validExtensions = ['.mp3', '.wav', '.aac', '.m4a', '.flac', '.ogg']
|
||||
const fileName = file.name.toLowerCase()
|
||||
const fileType = file.type.toLowerCase()
|
||||
|
||||
const isValidType = validTypes.some(type => fileType.includes(type)) ||
|
||||
|
||||
const isValidType = validTypes.some(type => fileType.includes(type)) ||
|
||||
validExtensions.some(ext => fileName.endsWith(ext))
|
||||
|
||||
|
||||
if (!isValidType) {
|
||||
message.error('请上传音频文件(MP3、WAV、AAC、M4A、FLAC、OGG)')
|
||||
return false
|
||||
}
|
||||
|
||||
return true // 允许添加到文件列表
|
||||
return true
|
||||
}
|
||||
|
||||
const handleCustomUpload = async (options) => {
|
||||
const { file, onSuccess, onError } = options
|
||||
|
||||
|
||||
uploading.value = true
|
||||
|
||||
|
||||
try {
|
||||
const res = await MaterialService.uploadFile(file, 'voice', null)
|
||||
|
||||
if (res.code === 0) {
|
||||
formData.fileId = res.data
|
||||
message.success('文件上传成功')
|
||||
|
||||
// 使用 nextTick 确保 DOM 更新完成后再调用回调
|
||||
await nextTick()
|
||||
|
||||
// 安全调用 onSuccess
|
||||
if (onSuccess && typeof onSuccess === 'function') {
|
||||
try {
|
||||
onSuccess(res, file)
|
||||
} catch (err) {
|
||||
console.warn('onSuccess 回调执行失败:', err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
if (res.code !== 0) {
|
||||
const errorMsg = res.msg || '上传失败'
|
||||
message.error(errorMsg)
|
||||
|
||||
// 安全调用 onError
|
||||
if (onError && typeof onError === 'function') {
|
||||
try {
|
||||
onError(new Error(errorMsg))
|
||||
} catch (err) {
|
||||
console.warn('onError 回调执行失败:', err)
|
||||
}
|
||||
}
|
||||
onError?.(new Error(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
formData.fileId = res.data
|
||||
message.success('文件上传成功')
|
||||
|
||||
await nextTick()
|
||||
onSuccess?.(res, file)
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
const errorMsg = error?.message || '上传失败,请稍后重试'
|
||||
message.error(errorMsg)
|
||||
|
||||
// 安全调用 onError
|
||||
if (onError && typeof onError === 'function') {
|
||||
try {
|
||||
onError(error)
|
||||
} catch (err) {
|
||||
console.warn('onError 回调执行失败:', err)
|
||||
}
|
||||
}
|
||||
onError?.(error)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
@@ -506,50 +440,50 @@ const handleRemoveFile = () => {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const params = isCreateMode.value
|
||||
? {
|
||||
name: formData.name,
|
||||
fileId: formData.fileId,
|
||||
autoTranscribe: formData.autoTranscribe,
|
||||
language: formData.language,
|
||||
gender: formData.gender,
|
||||
note: formData.note
|
||||
}
|
||||
: {
|
||||
id: formData.id,
|
||||
name: formData.name,
|
||||
language: formData.language,
|
||||
gender: formData.gender,
|
||||
note: formData.note,
|
||||
transcription: formData.transcription
|
||||
}
|
||||
submitting.value = true
|
||||
|
||||
const params = isCreateMode.value
|
||||
? {
|
||||
name: formData.name,
|
||||
fileId: formData.fileId,
|
||||
autoTranscribe: formData.autoTranscribe,
|
||||
language: formData.language,
|
||||
gender: formData.gender,
|
||||
note: formData.note
|
||||
}
|
||||
: {
|
||||
id: formData.id,
|
||||
name: formData.name,
|
||||
language: formData.language,
|
||||
gender: formData.gender,
|
||||
note: formData.note,
|
||||
transcription: formData.transcription
|
||||
}
|
||||
|
||||
try {
|
||||
const res = isCreateMode.value
|
||||
? await VoiceService.create(params)
|
||||
: await VoiceService.update(params)
|
||||
|
||||
if (res.code === 0) {
|
||||
message.success(isCreateMode.value ? '创建成功' : '更新成功')
|
||||
modalVisible.value = false
|
||||
|
||||
// 如果开启了自动识别,开始轮询识别结果
|
||||
if (isCreateMode.value && formData.autoTranscribe && res.data) {
|
||||
const voiceId = res.data
|
||||
message.info('自动识别已启动,正在识别中...')
|
||||
startPollingTranscription(voiceId)
|
||||
}
|
||||
|
||||
loadVoiceList()
|
||||
} else {
|
||||
if (res.code !== 0) {
|
||||
message.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.errorFields) {
|
||||
// 表单验证失败,不显示错误
|
||||
return
|
||||
}
|
||||
|
||||
message.success(isCreateMode.value ? '创建成功' : '更新成功')
|
||||
modalVisible.value = false
|
||||
|
||||
if (isCreateMode.value && formData.autoTranscribe && res.data) {
|
||||
message.info('自动识别已启动,正在识别中...')
|
||||
startPollingTranscription(res.data)
|
||||
}
|
||||
|
||||
loadVoiceList()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
message.error('操作失败,请稍后重试')
|
||||
} finally {
|
||||
@@ -596,26 +530,18 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.search-bar,
|
||||
.table-container {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@@ -630,20 +556,10 @@ onUnmounted(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.uploaded-file-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user