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

1278 lines
37 KiB
Vue
Raw Normal View History

2025-07-18 16:38:18 +08:00
<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="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-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>
<!-- 上传文件对话框 -->
<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">
2025-07-21 17:39:30 +08:00
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.indexingTechnique')" label-width="auto">
2025-07-18 16:38:18 +08:00
<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>
2025-07-21 17:39:30 +08:00
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.preProcessingRules')" label-width="auto">
2025-07-18 16:38:18 +08:00
<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>
2025-07-21 17:39:30 +08:00
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.segmentation')" label-width="auto">
2025-07-18 16:38:18 +08:00
<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-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 } from 'vue'
import VueOfficePdf from '@vue-office/pdf'
import { getDatasetDocPage, uploadDocument, deleteDocument, downloadDocument, previewDocumentUrl, renameDocument} 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
} 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
})
// 重命名相关
const renameDialogVisible = ref(false)
const renameForm = reactive({
id: '',
newName: ''
})
const getFileTypeIcon = (fileType: string) => {
// 使用动态导入获取图标路径
// const getIconUrl = (iconName: string) => {
// return new URL(require(`@/assets/img/filetype-icon/${iconName}.png`), import.meta.url).href
// }
2025-07-18 16:38:18 +08:00
const getIconUrl = (iconName: string) => {
return new URL(`/src/assets/img/filetype-icon/${iconName}.png`, import.meta.url).href
}
2025-07-18 16:38:18 +08:00
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,
2025-07-18 16:38:18 +08:00
})], {
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()
} 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 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(() => {
datasetId.value = route.params.id as string
datasetName.value = route.query.name as string
// 这里可以调用API获取详细数据
fetchDocuments()
})
// 分页变化处理
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;
}
}
.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;
}
.batch-delete-btn {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.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;
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
// 响应式设计
@media (max-width: 768px) {
.main-container {
padding: 12px;
}
.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>