ai-manus/chat-client/src/views/datasets/components/DocumentList.vue

1972 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="main-container">
<div class="page-header">
<h2 class="page-title">{{ datasetName }}</h2>
<div class="page-description">
{{t('vabI18n.knowledge.document.header.description')}} {{ pagination.total }} {{ $t(pagination.total > 1
? 'vabI18n.knowledge.document.header.descriptionEnds'
: 'vabI18n.knowledge.document.header.descriptionEnd') }}
</div>
</div>
<!-- 主要内容区域 -->
<div class="content-layout" :class="{ 'has-sidebar': showTaskSidebar }">
<div class="file-manager-container">
<!-- 操作栏 -->
<div class="action-bar">
<div class="left-group">
<el-input
v-model="searchKeyword"
:placeholder="$t('vabI18n.knowledge.document.search.placeholder')"
class="search-input"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" :icon="Search" @click="handleSearch">{{t('vabI18n.knowledge.document.buttons.search')}}</el-button>
</div>
<div class="right-group">
<el-button
v-if="selectedRows.length > 0"
type="danger"
:icon="Delete"
@click="handleBatchDelete"
class="batch-delete-btn"
>
{{t('vabI18n.knowledge.document.buttons.batchDelete', { count: selectedRows.length })}}
</el-button>
<el-button :icon="Refresh" @click="handleRefresh">{{t('vabI18n.knowledge.document.buttons.refresh')}}</el-button>
<!-- 任务监控切换按钮 -->
<el-tooltip
:content="showTaskSidebar ?
t('vabI18n.knowledge.document.tooltips.hideSidebar') :
t('vabI18n.knowledge.document.tooltips.showSidebar')"
placement="top"
>
<el-button
type="info"
@click="showTaskSidebar ? hideSidebar() : showSidebar()"
:class="{ 'has-running-tasks': hasRunningTasks }"
>
<el-icon v-if="hasRunningTasks" class="rotating-icon"><Loading /></el-icon>
<el-icon v-else><DataAnalysis /></el-icon>
{{t('vabI18n.knowledge.document.buttons.taskMonitor')}}
<el-badge v-if="hasRunningTasks" :value="runningTasksCount" class="task-badge" />
</el-button>
</el-tooltip>
<el-button type="primary" :icon="Upload" @click="triggerFileInput">{{t('vabI18n.knowledge.document.buttons.upload')}}</el-button>
</div>
</div>
<!-- 文件表格 -->
<el-table
v-loading="tableLoading"
:element-loading-text="$t('vabI18n.knowledge.document.messages.loading')"
:data="fileList"
stripe
class="file-table"
:row-style="{ height: '50px' }"
:header-row-style="{ height: '50px' }"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column
:label="$t('vabI18n.knowledge.document.table.index')"
width="80"
align="center"
type="index"
:index="indexMethod"
/>
<el-table-column prop="fileName" :label="t('vabI18n.knowledge.document.table.fileName')" min-width="250">
<template #default="{ row }">
<div class="file-name-cell">
<img
:src="getFileTypeIcon(row.fileType)"
alt="文件图标"
class="file-icon"
/>
<span class="file-name" :title="row.fileName">{{ row.fileName }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="parseStatus" :label="t('vabI18n.knowledge.document.table.status')" width="120" align="center">
<template #default="{ row }">
<el-tag type="success" v-if="row.parseStatus === t('vabI18n.knowledge.document.table.statusText.available')">
<el-icon><Check /></el-icon>
{{ row.parseStatus }}
</el-tag>
<el-tag v-else-if="row.parseStatus === t('vabI18n.knowledge.document.table.statusText.indexing')" type="warning">
<el-icon class="is-loading"><Loading /></el-icon>
{{ t('vabI18n.knowledge.document.table.statusText.indexing') }}
</el-tag>
<el-tag v-else type="danger">
<el-icon><Close /></el-icon>
{{ row.parseStatus }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createDate" :label="t('vabI18n.knowledge.document.table.createDate')" width="180" />
<el-table-column prop="charCount" :label="t('vabI18n.knowledge.document.table.charCount')" width="120" align="right">
<template #default="{ row }">
<span class="char-count">{{ formatNumber(row.charCount) }}</span>
</template>
</el-table-column>
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="320" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button
type="primary"
:icon="View"
text
class="action-btn"
@click="handlePreview(row)"
v-if="row.parseStatus === t('vabI18n.knowledge.document.table.statusText.available')"
>
{{t('vabI18n.knowledge.document.buttons.preview')}}
</el-button>
<el-button
type="success"
:icon="Download"
text
class="action-btn"
@click="handleDownload(row)"
>
{{t('vabI18n.knowledge.document.buttons.download')}}
</el-button>
<el-button
type="warning"
:icon="Edit"
text
class="action-btn"
@click="handleRename(row)"
>
{{t('vabI18n.knowledge.document.buttons.rename')}}
</el-button>
<el-button
type="danger"
:icon="Delete"
text
class="action-btn"
@click="handleDelete(row)"
>
{{t('vabI18n.knowledge.document.buttons.delete')}}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePaginationChange"
@size-change="handlePaginationChange"
/>
</div>
</div>
<!-- 右侧任务面板 -->
<transition name="slide-fade" mode="out-in">
<div v-if="showTaskSidebar" class="task-sidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<el-icon class="rotating-icon"><Loading /></el-icon>
<span>{{t('vabI18n.knowledge.document.sidebar.title')}}</span>
<el-badge :value="runningTasksCount" class="task-count-badge" />
</div>
<div class="sidebar-actions">
<el-button
text
:icon="Refresh"
@click="fetchDeepAnalysisList"
:loading="deepAnalysisLoading"
size="small"
>
{{t('vabI18n.knowledge.document.buttons.refresh')}}
</el-button>
<el-button
text
:icon="Close"
@click="hideSidebar"
size="small"
/>
</div>
</div>
<div class="sidebar-content">
<!-- 快速统计 -->
<div class="quick-stats">
<div class="stat-card">
<div class="stat-number">{{ runningTasksCount }}</div>
<div class="stat-label">{{t('vabI18n.knowledge.document.deepAnalysisDialog.running')}}</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ completedTasksCount }}</div>
<div class="stat-label">{{t('vabI18n.knowledge.document.deepAnalysisDialog.completed')}}</div>
</div>
</div>
<!-- 任务列表 -->
<div class="sidebar-task-list">
<div v-for="task in deepAnalysisList" :key="task.taskId" class="sidebar-task-item" :class="{ 'completed': task.percent >= 100 }">
<div class="task-header">
<div class="task-name" :title="task.name">{{ task.name }}</div>
<div class="task-percent">{{ Math.round(task.percent) }}%</div>
</div>
<el-progress
:percentage="Math.round(task.percent)"
:status="getProgressStatus(task.percent)"
:stroke-width="4"
:show-text="false"
class="task-progress-bar"
/>
<div class="task-meta">
<el-tag size="small" :type="getTaskStatusType(task.percent)" class="task-status-tag">
{{ getTaskStatusText(task.percent) }}
</el-tag>
<span class="task-time">{{ formatTimeAgo(task.createTime) }}</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="deepAnalysisList.length === 0" class="sidebar-empty">
<el-icon size="48" color="#c0c4cc"><Document /></el-icon>
<p>{{t('vabI18n.knowledge.document.deepAnalysisDialog.noTasks')}}</p>
</div>
</div>
</div>
</transition>
</div>
<!-- 上传文件对话框 -->
<el-dialog v-model="uploadDialogVisible" :title="t('vabI18n.knowledge.document.buttons.upload')" width="600px" class="upload-dialog">
<el-form :model="uploadForm" label-width="120px" class="upload-form">
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.indexingTechnique')" label-width="auto">
<el-radio-group v-model="uploadForm.indexingTechnique">
<el-radio value="high_quality">{{t('vabI18n.knowledge.document.uploadDialog.indexingOptions.highQuality')}}</el-radio>
<el-radio value="economy">{{t('vabI18n.knowledge.document.uploadDialog.indexingOptions.economy')}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.preProcessingRules')" label-width="auto">
<el-checkbox-group v-model="uploadForm.preProcessingRules">
<el-checkbox value="remove_extra_spaces">{{t('vabI18n.knowledge.document.uploadDialog.ruleOptions.removeSpaces')}}</el-checkbox>
<el-checkbox value="remove_urls_emails">{{t('vabI18n.knowledge.document.uploadDialog.ruleOptions.removeUrls')}}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.segmentation')" label-width="auto">
<div class="segment-rule">
<el-input
v-model="uploadForm.segmentSeparator"
:placeholder="t('vabI18n.knowledge.document.uploadDialog.ruleOptions.segmentSeparator')"
style="width: 200px; margin-right: 15px;"
/>
<el-input-number
v-model="uploadForm.segmentMaxTokens"
:min="100"
:max="2000"
:placeholder="t('vabI18n.knowledge.document.uploadDialog.maxTokens')"
/>
</div>
</el-form-item>
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.deepAnalysis')" label-width="auto">
<el-checkbox v-model="uploadForm.deepAnalysis">
{{t('vabI18n.knowledge.document.uploadDialog.deepAnalysisOption')}}
</el-checkbox>
</el-form-item>
<el-upload
ref="uploadRef"
multiple
:auto-upload="false"
:on-change="handleFileChange"
:file-list="uploadFiles"
:limit="10"
accept=".txt,.md,.markdown,.mdx,.pdf,.html,.htm,.xlsx,.xls,.docx,.csv,.vtt,.properties"
class="upload-component"
>
<el-button type="primary">{{t('vabI18n.knowledge.document.uploadDialog.selectFile')}}</el-button>
<template #tip>
<div class="el-upload__tip">
{{t('vabI18n.knowledge.document.uploadDialog.fileTip')}}
</div>
</template>
</el-upload>
</el-form>
<template #footer>
<el-button @click="handleCancelUpload">{{t('vabI18n.knowledge.document.uploadDialog.cancel')}}</el-button>
<el-button type="primary" @click="handleUpload" :loading="uploading">{{t('vabI18n.knowledge.document.uploadDialog.upload')}}</el-button>
</template>
</el-dialog>
<!-- 预览抽屉 -->
<el-drawer
v-model="previewDrawerVisible"
:title="t('vabI18n.knowledge.document.preview.title')"
:direction="'rtl'"
size="50%"
class="preview-drawer"
>
<div v-loading="previewLoading">
<!-- TXT 文件预览 -->
<div v-if="previewFileType === 'txt'" class="text-preview-container">
<pre class="text-preview">{{ previewTextContent }}</pre>
</div>
<!-- Markdown 文件预览 -->
<div v-if="['md', 'markdown', 'mdx'].includes(previewFileType.toLowerCase())"
class="markdown-preview-container">
<div class="markdown-preview" v-html="previewMarkdownContent"></div>
</div>
<vue-office-pdf
v-if="previewFileType === 'pdf'"
:src="previewFileUrl"
@rendered="renderedHandler"
@error="errorHandler"
/>
<vue-office-docx
v-if="previewFileType == 'doc' || previewFileType == 'docx'"
:src="previewFileUrl"
@rendered="renderedHandler"
@error="errorHandler"
/>
<vue-office-excel
v-if="previewFileType == 'xlsx' || previewFileType == 'xls' || previewFileType == 'csv'"
:src="previewFileUrl"
style="height: 100vh;"
@rendered="renderedHandler"
@error="errorHandler"
/>
</div>
</el-drawer>
<!-- 重命名对话框 -->
<el-dialog v-model="renameDialogVisible" :title="t('vabI18n.knowledge.document.renameDialog.title')" width="400px">
<el-form :model="renameForm" label-width="80px">
<el-form-item :label="t('vabI18n.knowledge.document.renameDialog.label')">
<el-input v-model="renameForm.newName" :placeholder="t('vabI18n.knowledge.document.renameDialog.placeholder')" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="renameDialogVisible = false">{{t('vabI18n.knowledge.document.renameDialog.cancel')}}</el-button>
<el-button type="primary" @click="handleRenameConfirm">{{t('vabI18n.knowledge.document.renameDialog.confirm')}}</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import VueOfficePdf from '@vue-office/pdf'
import { getDatasetDocPage, uploadDocument, deleteDocument, downloadDocument, previewDocumentUrl, renameDocument, getDeepAnalysisList } from '@/api/dataset'
//引入VueOfficeDocx组件
import VueOfficeDocx from '@vue-office/docx'
//引入相关样式
import '@vue-office/docx/lib/index.css'
//引入VueOfficeExcel组件
import VueOfficeExcel from '@vue-office/excel'
//引入相关样式
import '@vue-office/excel/lib/index.css'
import VueOfficePptx from '@vue-office/pptx'
// 引入 markdown 解析器和代码高亮
import { marked } from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
import { ElLoading, ElMessage, ElNotification, ElMessageBox } from 'element-plus'
import type { UploadFile as ElUploadFile, UploadFiles } from 'element-plus'
import {
Search,
Refresh,
Upload,
Download,
View,
Edit,
Delete,
Check,
Close,
Loading,
DataAnalysis,
Document
} from '@element-plus/icons-vue'
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
// 添加uploadRef引用
const uploadRef = ref()
const userStore = useUserStore()
const { token } = userStore
const route = useRoute();
const datasetId = ref('');
const datasetName = ref('');
const previewLoading = ref(true)
const uploadDialogVisible = ref(false)
const uploading = ref(false)
const uploadFiles = ref<ElUploadFile[]>([])
const uploadForm = reactive({
indexingTechnique: 'high_quality',
preProcessingRules: ['remove_extra_spaces', 'remove_urls_emails'],
segmentSeparator: '###',
segmentMaxTokens: 500,
deepAnalysis: false
})
// 重命名相关
const renameDialogVisible = ref(false)
const renameForm = reactive({
id: '',
newName: ''
})
// 深度解析相关
const deepAnalysisLoading = ref(false)
const deepAnalysisList = ref<PdfTask[]>([])
// 实时进度监控相关
const progressPollingTimer = ref<NodeJS.Timeout | null>(null)
const showTaskSidebar = ref(false)
const POLLING_INTERVAL = 3000 // 3秒轮询一次
// 定义深度解析任务类型
interface PdfTask {
name: string
taskId: string
percent: number
datasetName: string
createTime: number
}
// 计算属性
const runningTasks = computed(() =>
deepAnalysisList.value.filter(task => task.percent < 100)
)
const hasRunningTasks = computed(() => runningTasks.value.length > 0)
const runningTasksCount = computed(() => runningTasks.value.length)
const completedTasksCount = computed(() =>
deepAnalysisList.value.filter(task => task.percent >= 100).length
)
// 进度状态获取
const getProgressStatus = (percent: number) => {
if (percent >= 100) return 'success'
if (percent > 0) return 'warning'
return 'exception'
}
const getTaskStatusType = (percent: number) => {
if (percent >= 100) return 'success'
if (percent > 0) return 'warning'
return 'info'
}
const getTaskStatusText = (percent: number) => {
if (percent >= 100) return t('vabI18n.knowledge.document.deepAnalysisDialog.completed')
if (percent > 0) return t('vabI18n.knowledge.document.deepAnalysisDialog.running')
return t('vabI18n.knowledge.document.deepAnalysisDialog.pending')
}
// 时间格式化
const formatTimeAgo = (timestamp: number): string => {
const now = Date.now()
const time = timestamp * 1000
const diff = now - time
const minutes = Math.floor(diff / (1000 * 60))
const hours = Math.floor(diff / (1000 * 60 * 60))
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days > 0) return t('vabI18n.knowledge.document.timeAgo.days', { count: days })
if (hours > 0) return t('vabI18n.knowledge.document.timeAgo.hours', { count: hours })
if (minutes > 0) return t('vabI18n.knowledge.document.timeAgo.minutes', { count: minutes })
return t('vabI18n.knowledge.document.timeAgo.justNow')
}
// 轮询控制
const startProgressPolling = () => {
if (progressPollingTimer.value) return
progressPollingTimer.value = setInterval(async () => {
await fetchDeepAnalysisList(false) // false表示静默获取不显示loading
// 如果没有正在运行的任务,停止轮询
if (!hasRunningTasks.value) {
stopProgressPolling()
}
}, POLLING_INTERVAL)
}
const stopProgressPolling = () => {
if (progressPollingTimer.value) {
clearInterval(progressPollingTimer.value)
progressPollingTimer.value = null
}
}
// 侧边栏控制
const hideSidebar = () => {
showTaskSidebar.value = false
}
const showSidebar = () => {
showTaskSidebar.value = true
}
// 监听运行中的任务变化
watch(hasRunningTasks, (newVal, oldVal) => {
if (newVal && !oldVal) {
// 有新的运行任务,开始轮询和显示侧边栏
startProgressPolling()
showTaskSidebar.value = true // 自动显示侧边栏
} else if (!newVal && oldVal) {
// 所有任务完成,停止轮询
stopProgressPolling()
// 显示完成通知
ElNotification({
title: t('vabI18n.knowledge.document.notifications.allTasksCompleted'),
message: t('vabI18n.knowledge.document.notifications.allTasksCompletedMessage'),
type: 'success',
duration: 5000
})
// 可选:任务完成后延迟隐藏侧边栏
setTimeout(() => {
showTaskSidebar.value = false
}, 3000)
}
})
// 修改 fetchDeepAnalysisList 方法
const fetchDeepAnalysisList = async (showLoading = true) => {
if (showLoading) {
deepAnalysisLoading.value = true
}
try {
const { data } = await getDeepAnalysisList()
const newList = data || []
// 检查是否有任务完成
if (deepAnalysisList.value.length > 0) {
const previousRunning = deepAnalysisList.value.filter(task => task.percent < 100)
const currentRunning = newList.filter(task => task.percent < 100)
const completedTasks = previousRunning.filter(prev =>
!currentRunning.some(curr => curr.taskId === prev.taskId)
)
// 为每个完成的任务显示通知
completedTasks.forEach(task => {
ElNotification({
title: t('vabI18n.knowledge.document.notifications.taskCompleted'),
message: t('vabI18n.knowledge.document.notifications.taskCompletedMessage', { name: task.name }),
type: 'success',
duration: 4000
})
})
}
deepAnalysisList.value = newList
} catch (error) {
console.error('获取深度解析任务列表失败:', error)
if (showLoading) {
ElNotification({
title: t('vabI18n.knowledge.document.errors.fetchDeepAnalysisFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.NoKnowError'),
type: 'error'
})
}
} finally {
if (showLoading) {
deepAnalysisLoading.value = false
}
}
}
// 组件卸载时清理定时器
onUnmounted(() => {
stopProgressPolling()
})
const getFileTypeIcon = (fileType: string) => {
const icons = require.context('@/assets/img/filetype-icon', false, /\.png$/)
// console.log(icons)
// console.log('所有图标 keys:', icons.keys())
const getIconUrl = (iconName: string) => {
const fullName = `./${iconName}.png`
if (icons.keys().includes(fullName)) {
return icons(fullName)
} else {
// console.warn('图标不存在:', fullName)
return ''
}
}
const iconMap: Record<string, string> = {
'pdf': getIconUrl('pdf'),
'doc': getIconUrl('word'),
'docx': getIconUrl('word'),
'xls': getIconUrl('excel'),
'xlsx': getIconUrl('excel'),
'ppt': getIconUrl('ppt'),
'pptx': getIconUrl('ppt'),
'txt': getIconUrl('txt'),
'md': getIconUrl('txt'),
'markdown': getIconUrl('txt'),
'mdx': getIconUrl('txt'),
'html': getIconUrl('txt'),
'htm': getIconUrl('txt'),
'csv': getIconUrl('excel'),
'vtt': getIconUrl('txt'),
'properties': getIconUrl('txt'),
'xml': getIconUrl('xml'),
'zip': getIconUrl('zip'),
'mp4': getIconUrl('video'),
'avi': getIconUrl('video'),
'mp3': getIconUrl('audio'),
'wav': getIconUrl('audio'),
'jpg': getIconUrl('img'),
'jpeg': getIconUrl('img'),
'png': getIconUrl('img'),
'gif': getIconUrl('img'),
}
return iconMap[fileType.toLowerCase()] || getIconUrl('unknown')
}
// 多选相关
const selectedRows = ref<FileItem[]>([])
interface FileItem {
id: string
fileName: string
fileType: string
parseStatus: string
previewConvert: string
size: string
charCount: number
createDate: string
status: string
}
// 搜索关键词
const searchKeyword = ref('')
// 分页数据
const pagination = reactive({
current: 1,
size: 20,
total: 0
})
// 文件列表数据
const fileList = ref<FileItem[]>([])
const fileInput = ref<HTMLInputElement>()
//预览右抽屉状态
const previewDrawerVisible = ref(false)
const previewFileType = ref('')
const previewFileUrl = ref('')
// 添加文本内容预览状态
const previewTextContent = ref('')
const previewMarkdownContent = ref('')
// 选择变化处理
const handleSelectionChange = (selection: FileItem[]) => {
selectedRows.value = selection
}
// 批量删除处理
const handleBatchDelete = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning(t('vabI18n.knowledge.document.messages.noFileSelected'))
return
}
try {
await ElMessageBox.confirm(
t('vabI18n.knowledge.document.messages.batchDeleteConfirm', {
count: selectedRows.value.length
}),
t('vabI18n.knowledge.document.messages.deleteConfirmTitle'),
{
confirmButtonText: t('vabI18n.knowledge.document.buttons.confirm'),
cancelButtonText: t('vabI18n.knowledge.document.buttons.cancel'),
type: 'warning'
}
)
const loading = ElLoading.service({
lock: true,
text: t('vabI18n.knowledge.document.messages.deleteing'),
background: 'rgba(0, 0, 0, 0.7)'
})
try {
// 批量删除
for (const row of selectedRows.value) {
await deleteDocument({
datasetId: datasetId.value,
documentId: row.id
})
}
ElNotification({
title: t('vabI18n.knowledge.document.messages.deleteSuccessOk'),
message: t('vabI18n.knowledge.document.messages.batchDeleteSuccess') + ` ${selectedRows.value.length} ` + t('vabI18n.knowledge.document.messages.batchDeleteSuccessEnd'),
type: 'success'
})
// 清空选择
selectedRows.value = []
// 刷新文件列表
await fetchDocuments()
} finally {
loading.close()
}
} catch (error) {
console.error('批量删除失败:', error)
ElNotification({
title: t('vabI18n.knowledge.document.errors.deleteFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.deleteFailed'),
type: 'error'
})
}
}
const originalExtension = ref('')
// 重命名处理
const handleRename = (row: FileItem) => {
renameForm.id = row.id
const parts = row.fileName.split('.')
if (parts.length > 1) {
originalExtension.value = parts.pop()!
renameForm.newName = parts.join('.')
} else {
originalExtension.value = ''
renameForm.newName = row.fileName
}
renameDialogVisible.value = true
}
// 重命名确认
const handleRenameConfirm = async () => {
const newName = (originalExtension.value
? renameForm.newName.trim() + '.' + originalExtension.value
: renameForm.newName.trim())
if (!newName || newName === '.') {
ElMessage.warning(t('vabI18n.knowledge.document.messages.FilenamecantEmpty'))
return
}
try {
await renameDocument({
documentId: renameForm.id,
newName: newName
})
ElNotification({
title: t('vabI18n.knowledge.document.errors.renameSuccess'),
message: t('vabI18n.knowledge.document.messages.renameSuccess'),
type: 'success'
})
renameDialogVisible.value = false
await fetchDocuments()
} catch (error) {
ElNotification({
title: t('vabI18n.knowledge.document.errors.renameFailed'),
message: t('vabI18n.knowledge.document.messages.renameFailed'),
type: 'error'
})
}
}
// 数字格式化
const formatNumber = (num: number) => {
return num.toLocaleString()
}
// 处理取消上传
const handleCancelUpload = () => {
uploadDialogVisible.value = false
uploadFiles.value = []
uploadRef.value?.clearFiles()
}
// 监听对话框关闭事件
watch(uploadDialogVisible, (newVal) => {
if (!newVal) {
uploadFiles.value = []
uploadRef.value?.clearFiles()
}
})
// 监听预览抽屉关闭事件,清理预览内容
watch(previewDrawerVisible, (newVal) => {
if (!newVal) {
previewTextContent.value = ''
previewMarkdownContent.value = ''
previewFileUrl.value = ''
previewFileType.value = ''
}
})
import { useUserStore } from '@/store/modules/user'
import vab from '~/library/plugins/vab'
// 事件处理函数
const renderedHandler = () => {
previewLoading.value = false
console.log("渲染完成")
}
const errorHandler = () => {
previewLoading.value = false
console.log("渲染失败")
}
const triggerFileInput = () => {
fileInput.value?.click()
uploadDialogVisible.value = true
}
const indexMethod = (index: number) => {
return (pagination.current - 1) * pagination.size + index + 1
}
// 文件选择处理
const handleFileChange = (file: ElUploadFile, files: UploadFiles) => {
uploadFiles.value = files
}
// 上传处理
const handleUpload = async () => {
if (uploadFiles.value.length === 0) {
ElMessage.warning(t('vabI18n.knowledge.document.messages.noUploadFile'))
return
}
const loading = ElLoading.service({
lock: true,
text: t('vabI18n.knowledge.document.messages.uploadLoading'),
background: 'rgba(0, 0, 0, 0.7)'
})
try {
// 构造请求数据
const processRule = {
rules: {
preProcessingRules: uploadForm.preProcessingRules.map(rule => ({
id: rule,
enabled: true
})),
segmentation: {
separator: uploadForm.segmentSeparator || '###',
maxTokens: uploadForm.segmentMaxTokens
}
},
mode: "custom"
}
// 遍历上传文件
for (const file of uploadFiles.value) {
const formData = new FormData()
formData.append('file', file.raw!)
formData.append('request', new Blob([JSON.stringify({
datasetId: datasetId.value,
indexingTechnique: uploadForm.indexingTechnique,
processRule: processRule,
deepAnalysis: uploadForm.deepAnalysis
})], {
type: 'application/json'
}))
await uploadDocument(formData)
}
ElNotification({
title: t('vabI18n.knowledge.document.messages.uploadSuccess'),
message: `${uploadFiles.value.length} ${t('vabI18n.knowledge.document.messages.uploadSuccessEnd')}`,
type: 'success'
})
uploadDialogVisible.value = false
await fetchDocuments()
// 如果启用了深度解析,开始监控进度
if (uploadForm.deepAnalysis) {
setTimeout(async () => {
await fetchDeepAnalysisList()
}, 2000) // 2秒后获取任务列表
}
} catch (error) {
console.error('上传错误:', error)
ElNotification({
title: t('vabI18n.knowledge.document.errors.uploadFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.uploadFailed'),
type: 'error'
})
} finally {
loading.close()
uploadFiles.value = []
uploadRef.value?.clearFiles()
}
}
//文件下载
const handleDownload = async (row: FileItem) => {
try {
const response = await downloadDocument(row.id, datasetId.value)
// 创建 Blob 对象
const blob = new Blob([response.data], { type: 'application/octet-stream' })
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = row.fileName // 使用文件原始名称
link.style.display = 'none'
document.body.appendChild(link)
link.click()
// 清理
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
ElNotification({
title: t('vabI18n.knowledge.document.errors.downloadFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.NoKnowError'),
type: 'error'
})
}
}
//获取预览路径
const fetchPreviewUrl = async (documentId: string) => {
const response = await previewDocumentUrl(documentId, datasetId.value)
console.log("response",response)
return response.data
}
// 获取文件文本内容用于txt和markdown预览
const fetchFileContent = async (documentId: string, fileType: string) => {
try {
// 对于txt和markdown文件我们需要获取原始文本内容
// 这里可以通过下载接口获取文件内容,然后转为文本
const response = await downloadDocument(documentId, datasetId.value)
// 将响应数据转为文本
if (response.data instanceof Blob) {
const text = await response.data.text()
return text
} else if (typeof response.data === 'string') {
return response.data
} else {
return JSON.stringify(response.data)
}
} catch (error) {
console.error('获取文件内容失败:', error)
throw error
}
}
const handlePreview = async (row: FileItem) => {
// 显示抽屉
previewDrawerVisible.value = true
previewLoading.value = true
console.log("row",row)
previewFileType.value = row.fileType;
try {
// 对于txt和markdown文件获取文本内容
if (['txt', 'md', 'markdown', 'mdx'].includes(row.fileType.toLowerCase())) {
const content = await fetchFileContent(row.id, row.fileType)
if (row.fileType.toLowerCase() === 'txt') {
previewTextContent.value = content
} else if (['md', 'markdown', 'mdx'].includes(row.fileType.toLowerCase())) {
// 配置并解析 markdown
const renderer = new marked.Renderer()
marked.setOptions({
breaks: true,
gfm: true
})
previewMarkdownContent.value = marked.parse(content) as string
}
} else {
// 其他文件类型使用原有逻辑
previewFileUrl.value = await fetchPreviewUrl(row.id)
console.log("previewFileUrl", previewFileUrl.value )
}
} catch (error) {
ElNotification({
title: t('vabI18n.knowledge.document.errors.previewFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.getFileContentFailed'),
type: 'error'
})
} finally {
previewLoading.value = false
}
}
// 在script setup部分添加loading状态
const tableLoading = ref(false)
// 修改fetchDocuments函数
const fetchDocuments = async () => {
tableLoading.value = true
try {
const params = {
datasetId: datasetId.value,
keyword: searchKeyword.value || '',
page: pagination.current,
limit: pagination.size
}
const { data } = await getDatasetDocPage(params)
// 映射API数据到表格结构
fileList.value = data.content.map((item: any) => ({
id: item.id,
fileName: item.name,
parseStatus: formatIndexStatus(item.displayStatus),
fileType: item.name.split('.').pop()?.toLowerCase() || '',
charCount: item.wordCount,
createDate: formatTimestamp(item.createdAt),
status: item.displayStatus
}))
pagination.total = data.total
pagination.current = data.pageNo
} catch (error) {
console.error('获取文档列表失败:', error)
ElNotification({
title: t('vabI18n.knowledge.document.errors.fetchFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.NoKnowError'),
type: 'error'
})
} finally {
tableLoading.value = false
}
}
// 添加刷新方法
const handleRefresh = () => {
searchKeyword.value = ''
pagination.current = 1
selectedRows.value = []
fetchDocuments()
}
const formatTimestampToLocaleString = (timestamp: number): string => {
const date = new Date(timestamp * 1000)
return date.toLocaleString()
}
// 工具函数
const formatTimestamp = (timestamp: number): string => {
const date = new Date(timestamp * 1000)
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN')
}
const formatIndexStatus = (status: string): string => {
const statusMap: Record<string, string> = {
'available': t('vabI18n.knowledge.document.table.statusText.available'),
'indexing': t('vabI18n.knowledge.document.table.statusText.indexing'),
'error': t('vabI18n.knowledge.document.table.statusText.error')
}
return statusMap[status] || t('vabI18n.knowledge.document.table.statusText.unknown')
}
const handleDelete = async (row: FileItem) => {
try {
// 确认对话框
await ElMessageBox.confirm(t('vabI18n.knowledge.document.messages.deleteConfirm'), t('vabI18n.knowledge.deleteConfirm.title'), {
confirmButtonText: t('vabI18n.knowledge.deleteConfirm.confirm'),
cancelButtonText: t('vabI18n.knowledge.deleteConfirm.cancel'),
type: 'warning'
})
// 调用删除接口
await deleteDocument({
datasetId: datasetId.value,
documentId: row.id
})
// 成功处理
ElNotification({
title: t('vabI18n.knowledge.document.messages.deleteSuccessOk'),
message: `t('vabI18n.knowledge.document.messages.deleteSuccess') "${row.fileName}" t('vabI18n.knowledge.document.messages.deleteSuccessEnd')`,
type: 'success'
})
// 刷新文件列表
await fetchDocuments()
} catch (error) {
console.error('删除文件失败:', error)
ElNotification({
title: t('vabI18n.knowledge.document.errors.deleteFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.NoKnowError'),
type: 'error'
})
}
}
onMounted(async () => {
datasetId.value = route.params.id as string
datasetName.value = route.query.name as string
// 这里可以调用API获取详细数据
await fetchDocuments()
// 初始化时获取深度解析任务列表
await fetchDeepAnalysisList(false)
})
// 分页变化处理
const handlePaginationChange = () => {
fetchDocuments()
}
// 搜索处理
const handleSearch = () => {
pagination.current = 1
fetchDocuments()
}
</script>
<style lang="scss" scoped>
.main-container {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.page-header {
margin-bottom: 24px;
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin: 0 0 8px 0;
}
.page-description {
font-size: 14px;
color: #909399;
margin: 0;
}
}
// 内容布局样式
.content-layout {
display: flex;
gap: 20px;
position: relative;
transition: all 0.3s ease;
&.has-sidebar {
.file-manager-container {
margin-right: 320px; // 为侧边栏留出空间
}
}
}
// 任务侧边栏样式
.task-sidebar {
position: fixed;
top: 120px;
right: 20px;
width: 320px;
height: calc(100vh - 140px);
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
border: 1px solid #e4e7ed;
z-index: 1000;
display: flex;
flex-direction: column;
overflow: hidden;
.sidebar-header {
padding: 16px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
.sidebar-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
.rotating-icon {
animation: spin 2s linear infinite;
}
.task-count-badge {
::v-deep .el-badge__content {
background: #f56c6c;
border: 2px solid white;
}
}
}
.sidebar-actions {
display: flex;
gap: 4px;
::v-deep .el-button {
color: white;
border: none;
background: rgba(255, 255, 255, 0.1);
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
}
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 20px;
.quick-stats {
display: flex;
gap: 12px;
margin-bottom: 20px;
.stat-card {
flex: 1;
text-align: center;
padding: 12px 8px;
background: linear-gradient(135deg, #f8f9ff 0%, #e8f4fd 100%);
border-radius: 8px;
border: 1px solid #e1e8f0;
.stat-number {
font-size: 20px;
font-weight: 700;
color: #409eff;
margin-bottom: 4px;
}
.stat-label {
font-size: 11px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
.sidebar-task-list {
.sidebar-task-item {
background: #fafafa;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
border: 1px solid #e4e7ed;
transition: all 0.3s ease;
&:hover {
background: #f0f9ff;
border-color: #409eff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
&:last-child {
margin-bottom: 0;
}
&.completed {
background: linear-gradient(135deg, #f0f9ff 0%, #e6fffa 100%);
border-color: #67c23a;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.task-name {
font-weight: 600;
color: #303133;
font-size: 13px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.task-percent {
font-weight: 700;
color: #409eff;
font-size: 12px;
white-space: nowrap;
}
}
.task-progress-bar {
margin-bottom: 8px;
}
.task-meta {
display: flex;
justify-content: space-between;
align-items: center;
.task-status-tag {
font-size: 11px;
}
.task-time {
font-size: 11px;
color: #909399;
}
}
}
}
.sidebar-empty {
text-align: center;
padding: 40px 20px;
color: #909399;
p {
margin-top: 12px;
font-size: 14px;
}
}
}
}
// 侧边栏动画
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.3s ease-in;
}
.slide-fade-enter-from {
transform: translateX(100%);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(100%);
opacity: 0;
}
.file-manager-container {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.left-group {
display: flex;
align-items: center;
gap: 12px;
}
.search-input {
width: 280px;
}
.right-group {
display: flex;
align-items: center;
gap: 12px;
// 深度解析按钮样式
.has-running-tasks {
background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
border: none;
color: white;
position: relative;
&:hover {
background: linear-gradient(135deg, #337ecc 0%, #529e2b 100%);
}
.rotating-icon {
animation: spin 2s linear infinite;
}
.task-badge {
position: absolute;
top: -8px;
right: -8px;
::v-deep .el-badge__content {
background: #f56c6c;
border: 2px solid white;
}
}
}
}
.batch-delete-btn {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.file-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
::v-deep .el-table__header {
background: #fafafa;
}
::v-deep .el-table__header th {
background: #fafafa;
color: #606266;
font-weight: 600;
border-bottom: 2px solid #e4e7ed;
height: 50px;
}
::v-deep .el-table__body tr {
transition: all 0.2s ease;
&:hover {
background: #f5f7fa !important;
}
}
::v-deep .el-table__body td {
height: 50px;
border-bottom: 1px solid #ebeef5;
}
}
.file-name-cell {
display: flex;
align-items: center;
gap: 10px;
.file-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
object-fit: contain;
}
.file-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
}
.char-count {
font-family: 'Consolas', 'Monaco', monospace;
font-weight: 500;
}
.action-buttons {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
.action-btn {
font-size: 12px;
padding: 4px 6px;
border-radius: 4px;
transition: all 0.2s ease;
white-space: nowrap;
flex-shrink: 0;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
}
.pagination-wrapper {
margin-top: 24px;
display: flex;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid #ebeef5;
}
.upload-dialog {
::v-deep .el-dialog__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
}
::v-deep .el-dialog__title {
color: white;
font-weight: 600;
}
}
.upload-form {
.segment-rule {
display: flex;
align-items: center;
gap: 15px;
}
.upload-component {
::v-deep .el-upload__tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
line-height: 1.4;
}
}
}
.preview-drawer {
::v-deep .el-drawer__header {
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
margin-bottom: 0;
padding: 20px;
.el-drawer__title {
font-weight: 600;
color: #303133;
}
}
}
// TXT 文件预览样式
.text-preview-container {
height: 100%;
overflow: auto;
padding: 20px;
background: #fafafa;
.text-preview {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
color: #303133;
background: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 16px;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
// Markdown 文件预览样式
.markdown-preview-container {
height: 100%;
overflow: auto;
padding: 20px;
background: #fafafa;
.markdown-preview {
background: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
// Markdown 内容样式
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
color: #24292e;
}
h1 {
font-size: 2em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
h3 {
font-size: 1.25em;
}
p {
margin-bottom: 16px;
line-height: 1.6;
color: #24292e;
}
ul, ol {
padding-left: 2em;
margin-bottom: 16px;
}
li {
margin-bottom: 0.25em;
}
blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 0 0 16px 0;
}
code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
margin-bottom: 16px;
code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
}
table {
border-spacing: 0;
border-collapse: collapse;
margin-bottom: 16px;
width: 100%;
overflow: auto;
th, td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
th {
font-weight: 600;
background-color: #f6f8fa;
}
tr:nth-child(2n) {
background-color: #f6f8fa;
}
}
img {
max-width: 100%;
height: auto;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
a {
color: #0366d6;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
}
}
// 状态标签动画
::v-deep .el-tag {
transition: all 0.3s ease;
.el-icon {
margin-right: 4px;
}
&.is-loading {
animation: spin 1s linear infinite;
}
}
// 深度解析对话框样式
.deep-analysis-dialog {
::v-deep .el-dialog__header {
background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
color: white;
border-radius: 8px 8px 0 0;
}
::v-deep .el-dialog__title {
color: white;
font-weight: 600;
}
}
// 任务统计样式
.task-statistics {
display: flex;
gap: 24px;
margin-bottom: 20px;
.stat-item {
flex: 1;
text-align: center;
padding: 16px;
background: linear-gradient(135deg, #f8f9ff 0%, #e8f4fd 100%);
border-radius: 8px;
border: 1px solid #e1e8f0;
.stat-number {
font-size: 24px;
font-weight: 700;
color: #409eff;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
.empty-state {
padding: 40px 20px;
text-align: center;
}
.task-list {
max-height: 500px;
overflow-y: auto;
padding: 10px 0;
}
.task-item {
border: 1px solid #ebeef5;
border-radius: 12px;
margin-bottom: 16px;
padding: 20px;
background: #fafafa;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
transform: translateY(-3px);
}
&.completed {
background: linear-gradient(135deg, #f0f9ff 0%, #e6fffa 100%);
border-color: #67c23a;
}
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24px;
}
.task-info {
flex: 1;
.task-name {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
word-break: break-all;
}
.task-dataset, .task-id, .task-time {
margin: 6px 0;
font-size: 13px;
color: #909399;
}
.task-dataset {
color: #67c23a;
font-weight: 500;
}
}
.task-progress {
flex: 0 0 220px;
.progress-status {
margin-top: 8px;
text-align: center;
.status-tag {
font-weight: 600;
}
}
::v-deep .el-progress__text {
font-weight: 600;
}
}
// 响应式设计
@media (max-width: 768px) {
.main-container {
padding: 12px;
}
.content-layout {
&.has-sidebar {
.file-manager-container {
margin-right: 0; // 移动端不预留侧边栏空间
}
}
}
.task-sidebar {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100vh;
z-index: 2000;
border-radius: 0;
.sidebar-content {
padding: 16px;
.quick-stats {
flex-direction: column;
gap: 8px;
.stat-card {
padding: 16px;
}
}
}
}
.action-bar {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.left-group,
.right-group {
justify-content: center;
}
.search-input {
width: 100%;
}
.action-buttons {
flex-direction: column;
gap: 4px;
}
}
</style>