feat:导出功能重构
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<el-popover
|
||||
:ref="el => setPopoverRef(index, el)"
|
||||
placement="bottom-end"
|
||||
:width="120"
|
||||
:show-arrow="false"
|
||||
@@ -86,14 +87,32 @@
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else-if="previewData?.type === 'markdown'" class="preview-markdown">
|
||||
<pre>{{ previewData?.content }}</pre>
|
||||
<div v-else-if="previewData?.isWord" id="docx-preview-container" class="preview-docx"></div>
|
||||
<!-- Markdown/Mindmap 预览容器 -->
|
||||
<div
|
||||
v-else-if="previewData?.type === 'markdown' || previewData?.type === 'mindmap'"
|
||||
class="preview-markdown"
|
||||
v-html="renderedMarkdown"
|
||||
></div>
|
||||
<div v-else-if="previewData?.isExcel" id="excel-preview-container" class="preview-excel">
|
||||
<el-table
|
||||
v-if="previewData?.excelData?.length > 1"
|
||||
:data="previewData.excelData.slice(1)"
|
||||
stripe
|
||||
border
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="(col, idx) in previewData.excelData[0]"
|
||||
:key="idx"
|
||||
:prop="String(idx)"
|
||||
:label="String(col)"
|
||||
/>
|
||||
</el-table>
|
||||
<div v-else class="excel-empty">暂无数据</div>
|
||||
</div>
|
||||
<div v-else-if="previewData?.file_url" class="preview-iframe">
|
||||
<iframe :src="previewData?.file_url" frameborder="0"></iframe>
|
||||
<div class="preview-tip">该文件类型暂不支持直接预览,请下载查看</div>
|
||||
</div>
|
||||
<div v-else class="preview-empty">无法预览此文件</div>
|
||||
<div v-else-if="previewData?.isPpt" id="ppt-preview-container" class="preview-ppt"></div>
|
||||
<div v-else class="preview-empty">该文件类型暂不支持预览,请下载查看</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="previewVisible = false">关闭</el-button>
|
||||
@@ -104,15 +123,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog/index.vue'
|
||||
import { useAgentsStore } from '@/stores'
|
||||
import { useAgentsStore, useConfigStoreHook } from '@/stores'
|
||||
import { useNotification } from '@/composables/useNotification'
|
||||
import api from '@/api'
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const notification = useNotification()
|
||||
|
||||
// Props 接收大任务ID(数据库主键)
|
||||
const props = defineProps<{
|
||||
@@ -175,8 +196,49 @@ const previewData = ref<{
|
||||
file_url?: string
|
||||
file_name?: string
|
||||
type: string
|
||||
isWord?: boolean
|
||||
isExcel?: boolean
|
||||
excelData?: any[]
|
||||
excelSheetName?: string
|
||||
isPpt?: boolean
|
||||
}>({ type: '' })
|
||||
|
||||
// Markdown 渲染
|
||||
const renderedMarkdown = computed(() => {
|
||||
if (previewData.value.content && (previewData.value.type === 'markdown' || previewData.value.type === 'mindmap')) {
|
||||
return marked(previewData.value.content)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// Popover 引用管理
|
||||
const popoverRefs = ref<Map<number, any>>(new Map())
|
||||
const setPopoverRef = (index: number, el: any) => {
|
||||
if (el) {
|
||||
popoverRefs.value.set(index, el)
|
||||
}
|
||||
}
|
||||
const closeAllPopovers = () => {
|
||||
popoverRefs.value.forEach(popover => {
|
||||
popover?.hide()
|
||||
})
|
||||
}
|
||||
|
||||
// 判断是否为 Word 文件
|
||||
const isWord = (ext?: string) => {
|
||||
return ['doc', 'docx'].includes(ext?.toLowerCase() || '')
|
||||
}
|
||||
|
||||
// 判断是否为 Excel 文件
|
||||
const isExcel = (ext?: string) => {
|
||||
return ['xlsx', 'xls', 'excel'].includes(ext?.toLowerCase() || '')
|
||||
}
|
||||
|
||||
// 判断是否为 PPT 文件
|
||||
const isPPT = (ext?: string) => {
|
||||
return ['pptx', 'ppt'].includes(ext?.toLowerCase() || '')
|
||||
}
|
||||
|
||||
// 格式化时间显示
|
||||
const formatTime = (timestamp: any): string => {
|
||||
if (!timestamp) return '未知时间'
|
||||
@@ -222,8 +284,8 @@ const fetchExportList = async () => {
|
||||
const result = await api.getExportList({ task_id: taskId })
|
||||
|
||||
// 转换为前端显示格式
|
||||
exportResults.value = result.list.map((item) => {
|
||||
const style = exportStyles.value.find((s) => s.type === item.export_type)
|
||||
exportResults.value = result.list.map(item => {
|
||||
const style = exportStyles.value.find(s => s.type === item.export_type)
|
||||
return {
|
||||
id: item.id,
|
||||
record_id: item.id,
|
||||
@@ -235,7 +297,7 @@ const fetchExportList = async () => {
|
||||
created_at: item.created_at,
|
||||
file_url: item.file_url,
|
||||
file_name: item.file_name,
|
||||
export_type: item.export_type,
|
||||
export_type: item.export_type
|
||||
}
|
||||
})
|
||||
|
||||
@@ -247,42 +309,165 @@ const fetchExportList = async () => {
|
||||
|
||||
// 预览文件
|
||||
const previewResult = async (result: ExportResult) => {
|
||||
closeAllPopovers() // 关闭所有 popover
|
||||
|
||||
const recordId = result.id || result.record_id
|
||||
if (!recordId) {
|
||||
ElMessage.error('无法获取文件ID')
|
||||
return
|
||||
}
|
||||
|
||||
// 先初始化数据,再显示对话框,避免渲染时属性不存在
|
||||
const fileUrl = result.file_url || ''
|
||||
const fileName = result.file_name || '文件预览'
|
||||
|
||||
// 从 file_name 中提取扩展名
|
||||
const ext = fileName.split('.').pop()?.toLowerCase() || ''
|
||||
|
||||
// 先初始化数据
|
||||
previewData.value = {
|
||||
content: '',
|
||||
file_url: '',
|
||||
file_name: result.file_name || '文件预览',
|
||||
type: result.type || ''
|
||||
file_url: fileUrl,
|
||||
file_name: fileName,
|
||||
type: result.type || '',
|
||||
isWord: false,
|
||||
isExcel: false,
|
||||
isPpt: false
|
||||
}
|
||||
previewVisible.value = true
|
||||
previewLoading.value = true
|
||||
|
||||
try {
|
||||
const data = await api.previewExport(recordId)
|
||||
previewData.value = {
|
||||
...previewData.value,
|
||||
...data,
|
||||
previewVisible.value = true
|
||||
|
||||
// 如果是 Word 文件,先获取数据
|
||||
if (isWord(ext)) {
|
||||
try {
|
||||
// 先显示loading
|
||||
previewLoading.value = true
|
||||
|
||||
// 调用 preview 接口获取文件内容(后端直接返回文件流)
|
||||
const response = await fetch(`/api/export/${recordId}/preview`)
|
||||
|
||||
if (!response.ok) {
|
||||
ElMessage.error('获取文件失败')
|
||||
previewLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 blob
|
||||
const blob = await response.blob()
|
||||
console.log('Word blob created, size:', blob.size)
|
||||
|
||||
if (blob.size === 0) {
|
||||
ElMessage.error('文件内容为空')
|
||||
previewLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭loading,让容器显示出来
|
||||
previewLoading.value = false
|
||||
previewData.value.isWord = true
|
||||
|
||||
// 等待 DOM 渲染容器
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 导入 docx-preview 并渲染
|
||||
const docxPreview = await import('docx-preview')
|
||||
|
||||
// 获取预览容器
|
||||
const container = document.getElementById('docx-preview-container')
|
||||
console.log('Container found:', !!container, container?.id)
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = ''
|
||||
// 使用 renderAsync 并等待完成,inWrapper: false 避免创建额外包装器
|
||||
await docxPreview.renderAsync(blob, container, undefined, {
|
||||
inWrapper: false
|
||||
})
|
||||
console.log('Word docx-preview rendering completed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Word 预览失败:', error)
|
||||
previewLoading.value = false
|
||||
ElMessage.error('Word 预览失败')
|
||||
}
|
||||
} else if (isExcel(ext)) {
|
||||
// Excel 文件预览
|
||||
try {
|
||||
previewLoading.value = true
|
||||
|
||||
const response = await fetch(`/api/export/${recordId}/preview`)
|
||||
|
||||
if (!response.ok) {
|
||||
ElMessage.error('获取文件失败')
|
||||
previewLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
console.log('Excel blob created, size:', blob.size)
|
||||
|
||||
if (blob.size === 0) {
|
||||
ElMessage.error('文件内容为空')
|
||||
previewLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 读取 Excel 文件
|
||||
const XLSX = await import('xlsx')
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
|
||||
|
||||
// 获取第一个sheet
|
||||
const firstSheetName = workbook.SheetNames[0]
|
||||
const worksheet = workbook.Sheets[firstSheetName]
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]
|
||||
|
||||
console.log('Excel data parsed, rows:', jsonData.length)
|
||||
|
||||
// 关闭loading,显示数据
|
||||
previewLoading.value = false
|
||||
previewData.value.isExcel = true
|
||||
previewData.value.excelData = jsonData
|
||||
previewData.value.excelSheetName = firstSheetName
|
||||
} catch (error) {
|
||||
console.error('Excel 预览失败:', error)
|
||||
previewLoading.value = false
|
||||
ElMessage.error('Excel 预览失败')
|
||||
}
|
||||
} else if (isPPT(ext)) {
|
||||
// PPT 文件预览 - 由于技术限制,暂时提示下载查看
|
||||
// 如需完整预览功能,可以考虑部署后使用 Microsoft Office Online 或其他方案
|
||||
ElMessage.info('PPT文件暂不支持预览,请下载查看')
|
||||
previewLoading.value = false
|
||||
} catch (error) {
|
||||
console.error('预览失败:', error)
|
||||
ElMessage.error('预览失败')
|
||||
previewLoading.value = false
|
||||
} else {
|
||||
// 其他文件类型(Markdown 等),调用 preview 接口获取内容
|
||||
try {
|
||||
previewLoading.value = true
|
||||
|
||||
const response = await fetch(`/api/export/${recordId}/preview`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
ElMessage.error(data.error)
|
||||
previewLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 设置内容
|
||||
if (data.content) {
|
||||
previewData.value.content = data.content
|
||||
previewData.value.type = data.type || ext
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预览内容失败:', error)
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览弹窗中的下载按钮
|
||||
const handlePreviewDownload = () => {
|
||||
const fileName = previewData.value?.file_name
|
||||
const recordId = fileName ? exportResults.value.find(
|
||||
(r) => r.file_name === fileName
|
||||
)?.id : null
|
||||
const recordId = fileName ? exportResults.value.find(r => r.file_name === fileName)?.id : null
|
||||
|
||||
if (recordId) {
|
||||
downloadById(recordId)
|
||||
@@ -303,6 +488,8 @@ const downloadById = async (recordId: number) => {
|
||||
|
||||
// 下载文件
|
||||
const downloadResult = (result: ExportResult) => {
|
||||
closeAllPopovers() // 关闭所有 popover
|
||||
|
||||
const recordId = result.id || result.record_id
|
||||
if (!recordId) {
|
||||
ElMessage.error('无法获取文件ID')
|
||||
@@ -314,6 +501,8 @@ const downloadResult = (result: ExportResult) => {
|
||||
|
||||
// 分享
|
||||
const shareResult = async (result: ExportResult) => {
|
||||
closeAllPopovers() // 关闭所有 popover
|
||||
|
||||
const recordId = result.id || result.record_id
|
||||
if (!recordId) {
|
||||
ElMessage.error('无法获取文件ID')
|
||||
@@ -334,6 +523,8 @@ const shareResult = async (result: ExportResult) => {
|
||||
|
||||
// 删除
|
||||
const deleteResult = (result: ExportResult) => {
|
||||
closeAllPopovers() // 关闭所有 popover
|
||||
|
||||
resultToDelete.value = result
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -354,9 +545,7 @@ const confirmDelete = async () => {
|
||||
const success = await api.deleteExport(recordId)
|
||||
if (success) {
|
||||
// 从列表中移除
|
||||
const index = exportResults.value.findIndex(
|
||||
(r) => (r.id || r.record_id) === recordId
|
||||
)
|
||||
const index = exportResults.value.findIndex(r => (r.id || r.record_id) === recordId)
|
||||
if (index > -1) {
|
||||
exportResults.value.splice(index, 1)
|
||||
}
|
||||
@@ -392,13 +581,22 @@ const handleSelect = async (item: ExportStyle) => {
|
||||
// 添加加载状态
|
||||
loadingTypes.value.push(item.type)
|
||||
|
||||
// 显示正在生成的通知
|
||||
const notifyId = notification.info(`正在生成${item.name}...`, '请稍候', {
|
||||
duration: 0, // 不自动关闭
|
||||
showProgress: true,
|
||||
progress: 0
|
||||
})
|
||||
|
||||
try {
|
||||
// 调用后端接口导出(导出整个大任务的执行结果)
|
||||
console.log('开始导出 - task_id:', taskId, 'type:', item.type)
|
||||
// 获取实际用户ID
|
||||
const userId = localStorage.getItem('user_id')
|
||||
const result = await api.exportTask({
|
||||
task_id: taskId,
|
||||
export_type: item.type,
|
||||
user_id: 'current_user', // TODO: 获取实际用户ID
|
||||
user_id: userId || ''
|
||||
})
|
||||
console.log('导出结果:', result)
|
||||
|
||||
@@ -414,15 +612,31 @@ const handleSelect = async (item: ExportStyle) => {
|
||||
created_at: new Date().toISOString(),
|
||||
file_url: result.file_url,
|
||||
file_name: result.file_name,
|
||||
export_type: item.type,
|
||||
export_type: item.type
|
||||
}
|
||||
|
||||
exportResults.value.unshift(newItem)
|
||||
|
||||
console.log('导出成功:', result)
|
||||
|
||||
// 更新通知为成功状态
|
||||
notification.updateProgressDetail(notifyId, `${item.name}生成完成`, result.file_name || item.name, 100, 100)
|
||||
// 3秒后关闭通知
|
||||
setTimeout(() => {
|
||||
notification.removeNotification(notifyId)
|
||||
}, 3000)
|
||||
|
||||
ElMessage.success(`导出成功: ${item.name}`)
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
|
||||
// 更新通知为失败状态
|
||||
notification.updateProgressDetail(notifyId, `${item.name}生成失败`, '请重试', 0, 100)
|
||||
// 5秒后关闭通知
|
||||
setTimeout(() => {
|
||||
notification.removeNotification(notifyId)
|
||||
}, 5000)
|
||||
|
||||
ElMessage.error(`导出失败: ${item.name}`)
|
||||
} finally {
|
||||
// 移除加载状态
|
||||
@@ -476,7 +690,6 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -625,17 +838,161 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.preview-markdown {
|
||||
min-height: 300px;
|
||||
max-height: 55vh;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
background: var(--color-bg-detail);
|
||||
border-radius: 8px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(h1) {
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
padding-left: 24px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
border-left: 4px solid var(--el-color-primary);
|
||||
margin: 8px 0;
|
||||
padding-left: 16px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
:deep(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 12px 0;
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid var(--el-border-color);
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// Mindmap 样式
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
:deep(.markmap-node) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.markmap-node:hover) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-excel {
|
||||
min-height: 300px;
|
||||
max-height: 55vh;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
background: var(--color-bg-detail);
|
||||
border-radius: 8px;
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
.excel-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-table__header th) {
|
||||
background: var(--el-fill-color-light) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-docx {
|
||||
min-height: 400px;
|
||||
max-height: 55vh;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
background: var(--color-text-detail);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
|
||||
Reference in New Issue
Block a user