将音频资产管理的文本转语音功能从标签页改为下拉选择模式,在知识库文件管理中将文档详情从路由跳转改为弹窗展示

This commit is contained in:
WUSIJIAN
2026-02-04 15:19:29 +08:00
parent 1458fb7d7e
commit d92ae700fa
3 changed files with 568 additions and 167 deletions

View File

@@ -6,9 +6,16 @@
:close-on-click-modal="false" :close-on-click-modal="false"
@close="handleClose" @close="handleClose"
> >
<el-tabs v-model="activeTab" class="audio-tabs"> <!-- 模式切换下拉菜单 -->
<el-tab-pane label="上传音频" name="upload"> <div class="mode-switch">
<el-form ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="100px"> <el-select v-model="activeMode" style="width: 160px">
<el-option label="上传音频" value="upload" />
<el-option label="文本转语音" value="tts" />
</el-select>
</div>
<!-- 上传音频模式 -->
<el-form v-if="activeMode === 'upload'" ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="100px" class="audio-form">
<el-form-item label="音频名称" prop="name"> <el-form-item label="音频名称" prop="name">
<el-input v-model="uploadForm.name" placeholder="请输入音频名称" maxlength="50" show-word-limit /> <el-input v-model="uploadForm.name" placeholder="请输入音频名称" maxlength="50" show-word-limit />
</el-form-item> </el-form-item>
@@ -53,10 +60,9 @@
</div> </div>
</div> </div>
</el-form> </el-form>
</el-tab-pane>
<el-tab-pane label="文本转语音" name="tts"> <!-- 文本转语音模式 -->
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-form v-else ref="formRef" :model="form" :rules="rules" label-width="100px" class="audio-form">
<el-form-item label="音频名称" prop="name"> <el-form-item label="音频名称" prop="name">
<el-input v-model="form.name" placeholder="请输入音频名称" maxlength="50" show-word-limit /> <el-input v-model="form.name" placeholder="请输入音频名称" maxlength="50" show-word-limit />
</el-form-item> </el-form-item>
@@ -129,7 +135,6 @@
<el-radio label="pcm">PCM</el-radio> <el-radio label="pcm">PCM</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-form>
<!-- TTS预览区域 --> <!-- TTS预览区域 -->
<div v-if="previewGenerated" class="preview-section"> <div v-if="previewGenerated" class="preview-section">
@@ -161,13 +166,12 @@
<span>采样率: {{ form.sampleRate }}Hz</span> <span>采样率: {{ form.sampleRate }}Hz</span>
</div> </div>
</div> </div>
</el-tab-pane> </el-form>
</el-tabs>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="handleClose">取消</el-button> <el-button @click="handleClose">取消</el-button>
<template v-if="activeTab === 'upload'"> <template v-if="activeMode === 'upload'">
<el-button type="primary" :loading="uploading" :disabled="!uploadPreview.show" @click="handleUpload"> <el-button type="primary" :loading="uploading" :disabled="!uploadPreview.show" @click="handleUpload">
<el-icon><ele-Upload /></el-icon> <el-icon><ele-Upload /></el-icon>
{{ uploading ? '上传中...' : '上传音频' }} {{ uploading ? '上传中...' : '上传音频' }}
@@ -196,7 +200,7 @@ import type { FormInstance, FormRules, UploadInstance, UploadFile, UploadRawFile
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const visible = ref(false); const visible = ref(false);
const activeTab = ref('upload'); const activeMode = ref<'upload' | 'tts'>('upload');
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const uploadFormRef = ref<FormInstance>(); const uploadFormRef = ref<FormInstance>();
const uploadRef = ref<UploadInstance>(); const uploadRef = ref<UploadInstance>();
@@ -260,7 +264,7 @@ const openDialog = () => {
// 重置表单 // 重置表单
const resetForm = () => { const resetForm = () => {
activeTab.value = 'upload'; activeMode.value = 'upload';
// 重置上传表单 // 重置上传表单
uploadForm.name = ''; uploadForm.name = '';
uploadForm.voiceType = ''; uploadForm.voiceType = '';
@@ -462,10 +466,12 @@ defineExpose({
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.audio-tabs { .mode-switch {
:deep(.el-tabs__content) { margin-bottom: 20px;
padding: 10px 0;
} }
.audio-form {
margin-top: 10px;
} }
.audio-uploader { .audio-uploader {

View File

@@ -0,0 +1,389 @@
<template>
<el-dialog
v-model="visible"
:title="documentInfo.name || '文档详情'"
width="90%"
top="5vh"
:close-on-click-modal="false"
destroy-on-close
class="document-detail-dialog"
>
<div class="dialog-content">
<!-- 左侧文档原文 -->
<div class="document-content">
<div class="content-header">
<h2>{{ documentInfo.name }}</h2>
<div class="content-meta">
Size{{ formatFileSize(documentInfo.fileSize) }} Uploaded Time{{ documentInfo.createdAt }}
</div>
</div>
<div class="content-body" v-loading="contentLoading">
<pre class="document-text">{{ documentContent }}</pre>
</div>
</div>
<!-- 右侧切片结果 -->
<div class="chunk-panel">
<div class="panel-header">
<h3>切片结果</h3>
<div class="panel-subtitle">查看用于嵌入和召回的切片段落</div>
</div>
<div class="panel-toolbar">
<div class="toolbar-tabs">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="full">全文</el-radio-button>
<el-radio-button label="chunk">省略</el-radio-button>
</el-radio-group>
</div>
<div class="toolbar-actions">
<el-input
v-model="chunkSearch"
placeholder="搜索"
clearable
size="small"
style="width: 120px"
>
<template #prefix>
<el-icon><ele-Search /></el-icon>
</template>
</el-input>
<el-button size="small" @click="onAddChunk">
<el-icon><ele-Plus /></el-icon>
</el-button>
</div>
</div>
<div class="chunk-list" v-loading="chunkLoading">
<div class="select-all">
<el-checkbox v-model="selectAll" @change="onSelectAllChange">选择所有</el-checkbox>
</div>
<div
class="chunk-item"
v-for="(chunk, index) in filteredChunks"
:key="chunk.id"
>
<div class="chunk-checkbox">
<el-checkbox v-model="chunk.selected" />
</div>
<div class="chunk-content">
<span class="chunk-text">{{ viewMode === 'full' ? chunk.content : truncateText(chunk.content, 100) }}</span>
</div>
<div class="chunk-actions">
<el-switch v-model="chunk.enabled" size="small" @change="onChunkStatusChange(chunk)" />
</div>
</div>
<el-empty v-if="filteredChunks.length === 0 && !chunkLoading" description="暂无切片" :image-size="60" />
</div>
<!-- 分页 -->
<div class="panel-footer">
<span class="total-info">总共 {{ chunkTotal }} </span>
<el-pagination
v-model:current-page="chunkPage"
:page-size="chunkPageSize"
:total="chunkTotal"
layout="prev, pager, next"
small
@current-change="getChunkList"
/>
<el-select v-model="chunkPageSize" size="small" style="width: 80px" @change="getChunkList">
<el-option :value="10" label="10条/页" />
<el-option :value="20" label="20条/页" />
<el-option :value="50" label="50条/页" />
</el-select>
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts">
export default {
name: 'documentDetailDialog',
};
</script>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps<{
modelValue: boolean;
datasetId: string;
datasetName: string;
document: any;
}>();
const emit = defineEmits(['update:modelValue']);
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
// 文档信息
const documentInfo = reactive({
id: '',
name: '',
fileType: '',
fileSize: 0,
createdAt: '',
});
// 文档内容
const documentContent = ref('');
const contentLoading = ref(false);
// 切片相关
const chunkLoading = ref(false);
const chunkList = ref<any[]>([]);
const chunkTotal = ref(0);
const chunkPage = ref(1);
const chunkPageSize = ref(50);
const chunkSearch = ref('');
const viewMode = ref('full');
const selectAll = ref(false);
// 过滤后的切片列表
const filteredChunks = computed(() => {
if (!chunkSearch.value) return chunkList.value;
return chunkList.value.filter(chunk =>
chunk.content.toLowerCase().includes(chunkSearch.value.toLowerCase())
);
});
// 格式化文件大小
const formatFileSize = (size: number) => {
if (!size) return '0 Bytes';
if (size < 1024) return size + ' Bytes';
if (size < 1024 * 1024) return (size / 1024).toFixed(0) + ' KB';
return (size / 1024 / 1024).toFixed(1) + ' MB';
};
// 截断文本
const truncateText = (text: string, maxLength: number) => {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
// 获取文档详情
const getDocumentDetail = async () => {
contentLoading.value = true;
try {
// 模拟数据
documentInfo.id = props.document?.id || '';
documentInfo.name = props.document?.name || '456_product(1).txt';
documentInfo.fileType = props.document?.fileType || 'txt';
documentInfo.fileSize = props.document?.fileSize || 10;
documentInfo.createdAt = props.document?.createdAt || '22/01/2026 00:53:32';
// 模拟文档内容
documentContent.value = '<p>123</p>';
} catch (error) {
console.error('获取文档详情失败:', error);
} finally {
contentLoading.value = false;
}
};
// 获取切片列表
const getChunkList = async () => {
chunkLoading.value = true;
try {
// 模拟数据
chunkList.value = [
{
id: '1',
content: '123',
enabled: true,
selected: false,
},
];
chunkTotal.value = 1;
} catch (error) {
console.error('获取切片列表失败:', error);
} finally {
chunkLoading.value = false;
}
};
// 全选变化
const onSelectAllChange = (val: boolean) => {
chunkList.value.forEach(chunk => {
chunk.selected = val;
});
};
// 切片状态变化
const onChunkStatusChange = (chunk: any) => {
ElMessage.success(chunk.enabled ? '已启用' : '已禁用');
};
// 添加切片
const onAddChunk = () => {
ElMessage.info('添加切片功能开发中');
};
// 监听弹窗打开
watch(() => props.modelValue, (val) => {
if (val && props.document) {
getDocumentDetail();
getChunkList();
}
});
</script>
<style scoped lang="scss">
.document-detail-dialog {
:deep(.el-dialog__body) {
padding: 0;
max-height: calc(90vh - 100px);
overflow: hidden;
}
}
.dialog-content {
display: flex;
height: 70vh;
// 左侧文档内容
.document-content {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #ebeef5;
overflow: hidden;
.content-header {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
h2 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.content-meta {
font-size: 12px;
color: #909399;
}
}
.content-body {
flex: 1;
padding: 16px 20px;
overflow: auto;
background: #fafafa;
.document-text {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.6;
color: #303133;
white-space: pre-wrap;
word-break: break-all;
}
}
}
// 右侧切片面板
.chunk-panel {
width: 420px;
display: flex;
flex-direction: column;
background: #fff;
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.panel-subtitle {
font-size: 12px;
color: #909399;
}
}
.panel-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #ebeef5;
.toolbar-actions {
display: flex;
gap: 8px;
}
}
.chunk-list {
flex: 1;
overflow: auto;
padding: 12px 20px;
.select-all {
margin-bottom: 12px;
}
.chunk-item {
display: flex;
align-items: flex-start;
padding: 12px;
background: #f5f7fa;
border-radius: 6px;
margin-bottom: 8px;
border: 1px solid #ebeef5;
.chunk-checkbox {
margin-right: 12px;
padding-top: 2px;
}
.chunk-content {
flex: 1;
min-width: 0;
.chunk-text {
font-size: 14px;
color: #303133;
line-height: 1.5;
word-break: break-all;
}
}
.chunk-actions {
margin-left: 12px;
flex-shrink: 0;
}
}
}
.panel-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 12px 20px;
border-top: 1px solid #ebeef5;
.total-info {
font-size: 12px;
color: #909399;
}
}
}
}
</style>

View File

@@ -323,6 +323,14 @@
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 文档详情弹窗 -->
<DocumentDetailDialog
v-model="showDocumentDetailDialog"
:datasetId="currentDataset?.id || ''"
:datasetName="currentDataset?.name || ''"
:document="currentDocument"
/>
</div> </div>
</template> </template>
@@ -337,6 +345,7 @@ import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules, UploadFile } from 'element-plus'; import type { FormInstance, FormRules, UploadFile } from 'element-plus';
import DocumentDetailDialog from './component/documentDetailDialog.vue';
const router = useRouter(); const router = useRouter();
@@ -364,6 +373,10 @@ const showUploadDialog = ref(false);
const uploadFileList = ref<UploadFile[]>([]); const uploadFileList = ref<UploadFile[]>([]);
const uploading = ref(false); const uploading = ref(false);
// 文档详情弹窗
const showDocumentDetailDialog = ref(false);
const currentDocument = ref<any>(null);
// 菜单相关 // 菜单相关
const activeMenu = ref('files'); const activeMenu = ref('files');
@@ -590,15 +603,8 @@ const onFileStatusChange = (row: any) => {
// 查看文档详情 // 查看文档详情
const onViewDocumentDetail = (row: any) => { const onViewDocumentDetail = (row: any) => {
router.push({ currentDocument.value = row;
path: '/knowledge/document/detail', showDocumentDetailDialog.value = true;
query: {
datasetId: currentDataset.value?.id,
datasetName: currentDataset.value?.name,
docId: row.id,
docName: row.name,
},
});
}; };
// 预览文件 // 预览文件