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

2466 lines
62 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">
<div class="header-top">
<el-button
type="text"
:icon="ArrowLeft"
@click="handleGoBack"
class="back-button"
>
{{t('vabI18n.knowledge.document.buttons.back')}}
</el-button>
<h2 class="page-title">{{ datasetName }}</h2>
</div>
<!-- 面包屑导航 -->
<div class="breadcrumb-container">
<el-breadcrumb separator="/">
<el-breadcrumb-item @click="navigateToRoot" class="breadcrumb-clickable">
<el-icon><House /></el-icon>
<span>{{t('vabI18n.knowledge.document.breadcrumb.root', '根目录')}}</span>
</el-breadcrumb-item>
<el-breadcrumb-item
v-for="(folder, index) in breadcrumbPath"
:key="folder.id"
@click="navigateToFolder(folder, index)"
:class="{ 'breadcrumb-clickable': index < breadcrumbPath.length - 1 }"
>
<span>{{ folder.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<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">
<div
class="file-manager-container"
@contextmenu.prevent="handleRightClick"
@click="hideContextMenu"
>
<!-- 操作栏 -->
<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"
@row-dblclick="handleRowDoubleClick"
@sort-change="handleSortChange"
>
<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" sortable="custom" :sort-orders="['ascending', 'descending']">
<template #default="{ row }">
<div class="file-name-cell">
<img
v-if="row.type === 'file'"
:src="getFileTypeIcon(row.fileType)"
alt="文件图标"
class="file-icon"
/>
<el-icon v-else class="folder-icon" size="20" color="#409eff">
<Folder />
</el-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 }">
<!-- 文件夹不显示状态 -->
<template v-if="row.type === 'file'">
<el-tag
:type="getStatusTagType(row.status)"
:class="{ 'rotating-tag': ['preprocessing', 'indexing', 'waiting', 'queued'].includes(row.status) }"
>
<el-icon
:class="{ 'is-loading': ['preprocessing', 'indexing', 'waiting', 'queued'].includes(row.status) }"
>
<component :is="getStatusIcon(row.status)" />
</el-icon>
{{ row.parseStatus }}
</el-tag>
</template>
<span v-else class="folder-status">--</span>
</template>
</el-table-column>
<el-table-column prop="createDate" :label="t('vabI18n.knowledge.document.table.createDate')" width="180" sortable="custom" :sort-orders="['ascending', 'descending']" />
<el-table-column prop="charCount" :label="t('vabI18n.knowledge.document.table.charCount')" width="120" align="right" sortable="custom" :sort-orders="['ascending', 'descending']">
<template #default="{ row }">
<span class="char-count" v-if="row.type === 'file'">{{ formatFileSize(row.charCount) }}</span>
<span v-else class="folder-indicator">--</span>
</template>
</el-table-column>
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="380" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<!-- 文件操作 -->
<template v-if="row.type === 'file'">
<el-button
:type="row.status === 'completed' ? 'primary' : 'info'"
:icon="View"
text
class="action-btn"
:disabled="row.status !== 'completed'"
:class="{ 'disabled-btn': row.status !== 'completed' }"
@click="row.status === 'completed' ? handlePreview(row) : null"
>
{{t('vabI18n.knowledge.document.buttons.preview')}}
</el-button>
<el-button
:type="row.status === 'completed' ? 'success' : 'info'"
:icon="Download"
text
class="action-btn"
:disabled="row.status !== 'completed'"
:class="{ 'disabled-btn': row.status !== 'completed' }"
@click="row.status === 'completed' ? handleDownload(row) : null"
>
{{t('vabI18n.knowledge.document.buttons.download')}}
</el-button>
<el-button
type="warning"
:icon="Upload"
text
class="action-btn"
@click="handleFileReplaceFromRow(row)"
>
替换
</el-button>
</template>
<!-- 文件夹操作 -->
<template v-else>
<el-button
type="primary"
:icon="View"
text
class="action-btn"
@click="handleOpenFolder(row)"
>
{{t('vabI18n.knowledge.document.buttons.open', '打开')}}
</el-button>
</template>
<!-- 共同操作 -->
<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>
</div>
<!-- 上传文件对话框 - 使用新的DocUpload组件 -->
<el-dialog
v-model="uploadDialogVisible"
:title="t('vabI18n.knowledge.document.buttons.upload')"
width="900px"
:close-on-click-modal="false"
class="upload-dialog"
>
<DocUpload
:visible="uploadDialogVisible"
:dataset-id="datasetId"
:parent-id="currentParentId"
@update:visible="uploadDialogVisible = $event"
@success="handleUploadSuccess"
/>
</el-dialog>
<!-- 文件替换对话框 -->
<el-dialog
v-model="replaceDialogVisible"
title="替换文件"
width="900px"
:close-on-click-modal="false"
class="upload-dialog"
>
<div v-if="replaceTargetFile" class="replace-info">
<div class="target-file-info">
<h4>将要替换的文件:</h4>
<div class="file-info-card">
<img
:src="getFileTypeIcon(replaceTargetFile.fileType)"
alt="文件图标"
class="file-icon"
/>
<div class="file-details">
<div class="file-name">{{ replaceTargetFile.fileName }}</div>
<div class="file-meta">
<span>大小:{{ formatFileSize(replaceTargetFile.charCount) }}</span>
<span>创建时间:{{ replaceTargetFile.createDate }}</span>
</div>
</div>
</div>
</div>
</div>
<DocUpload
:visible="replaceDialogVisible"
:dataset-id="datasetId"
:parent-id="currentParentId"
:replace-file-id="replaceTargetFile?.id"
@update:visible="replaceDialogVisible = $event"
@success="handleReplaceSuccess"
/>
</el-dialog>
<!-- 预览抽屉 -->
<el-drawer
v-model="previewDrawerVisible"
:title="t('vabI18n.knowledge.document.preview.title')"
:direction="'rtl'"
size="80%"
class="preview-drawer"
:close-on-click-modal="false"
@close="handleClose"
>
<div v-loading="previewLoading" class="drawer-content-container" style="display: flex; height: 100%;">
<!-- 左侧分段数据容器 -->
<div class="segment-list-container" style="width: 30%; border-right: 1px solid #ebeef5; overflow-y: auto; padding: 10px;">
<h3>分段数据</h3>
<el-form v-if="segmentList.length > 0">
<el-form-item
v-for="(segment) in segmentList"
:key="segment.id"
style="cursor: pointer; margin-bottom: 5px;"
>
<span>分段{{segment.position}}</span>
<h4>{{segment.content}}</h4>
</el-form-item>
</el-form>
<div v-else>
暂无分段数据
</div>
</div>
<!-- 右侧原预览内容 -->
<div class="preview-content-container" style="flex: 1; padding: 10px; overflow-y: auto;">
<!-- 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>
</div>
</el-drawer>
<!-- 重命名对话框 -->
<el-dialog
v-model="renameDialogVisible"
:title="t('vabI18n.knowledge.document.renameDialog.title')"
width="480px"
class="rename-dialog"
:close-on-click-modal="false"
align-center
>
<div class="dialog-content">
<div class="dialog-icon">
<el-icon size="48" color="#409eff"><Edit /></el-icon>
</div>
<el-form :model="renameForm" label-width="0px" class="rename-form">
<el-form-item>
<div class="form-label">{{t('vabI18n.knowledge.document.renameDialog.label')}}</div>
<el-input
v-model="renameForm.newName"
:placeholder="t('vabI18n.knowledge.document.renameDialog.placeholder')"
size="large"
class="rename-input"
@keyup.enter="handleRenameConfirm"
>
<template #prefix>
<el-icon><Document /></el-icon>
</template>
</el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button size="large" @click="renameDialogVisible = false">
{{t('vabI18n.knowledge.document.renameDialog.cancel')}}
</el-button>
<el-button type="primary" size="large" @click="handleRenameConfirm">
<el-icon><Check /></el-icon>
{{t('vabI18n.knowledge.document.renameDialog.confirm')}}
</el-button>
</div>
</template>
</el-dialog>
<!-- 创建文件夹对话框 -->
<el-dialog
v-model="createFolderDialogVisible"
:title="t('vabI18n.knowledge.document.createFolderDialog.title', '创建文件夹')"
width="520px"
class="create-folder-dialog"
:close-on-click-modal="false"
align-center
>
<div class="dialog-content">
<div class="dialog-icon">
<el-icon size="48" color="#67c23a"><FolderAdd /></el-icon>
</div>
<el-form :model="createFolderForm" :rules="createFolderRules" ref="createFolderFormRef" label-width="0px" class="create-folder-form">
<el-form-item prop="folderName">
<div class="form-label">{{t('vabI18n.knowledge.document.createFolderDialog.label', '文件夹名称')}}</div>
<el-input
v-model="createFolderForm.folderName"
:placeholder="t('vabI18n.knowledge.document.createFolderDialog.placeholder', '请输入文件夹名称')"
size="large"
class="folder-name-input"
@keyup.enter="handleCreateFolderConfirm"
>
<template #prefix>
<el-icon><Folder /></el-icon>
</template>
</el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button size="large" @click="createFolderDialogVisible = false">
{{t('vabI18n.knowledge.document.createFolderDialog.cancel', '取消')}}
</el-button>
<el-button
type="primary"
size="large"
@click="handleCreateFolderConfirm"
:loading="createFolderLoading"
>
<el-icon><Plus /></el-icon>
{{t('vabI18n.knowledge.document.createFolderDialog.confirm', '创建')}}
</el-button>
</div>
</template>
</el-dialog>
<!-- 右键菜单 -->
<transition name="context-menu-fade">
<div
v-if="contextMenuVisible"
class="context-menu"
:style="{ left: contextMenuPosition.x + 'px', top: contextMenuPosition.y + 'px' }"
@click.stop
>
<div class="context-menu-header">
<div class="menu-title">操作菜单</div>
</div>
<div class="context-menu-item" @click="handleRefresh">
<div class="item-icon">
<el-icon><Refresh /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{t('vabI18n.knowledge.document.contextMenu.refresh', '刷新')}}</div>
<div class="item-desc">刷新文件列表</div>
</div>
</div>
<div class="context-menu-item" @click="handleCreateFolderFromContext">
<div class="item-icon">
<el-icon><FolderAdd /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{t('vabI18n.knowledge.document.contextMenu.createFolder', '新建文件夹')}}</div>
<div class="item-desc">在当前目录创建文件夹</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import VueOfficePdf from '@vue-office/pdf'
import { getDatasetDocPage, uploadDocument, deleteDocument, downloadDocument, previewDocumentUrl, renameDocument, createFolder, CreateFolderReq} from '@/api/dataset'
import {getSegmentList} from "@/api/Segment"
import DocUpload from './DocUpload.vue'
//引入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 {
Search,
Refresh,
Upload,
Download,
View,
Edit,
Delete,
Check,
Close,
Loading,
Document,
InfoFilled,
ArrowLeft,
Folder,
FolderAdd,
House,
Plus
} from '@element-plus/icons-vue'
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const userStore = useUserStore()
const aclStore = useAclStore()
const { token } = userStore
const route = useRoute();
const router = useRouter();
const datasetId = ref('');
const datasetName = ref('');
const previewLoading = ref(true)
const uploadDialogVisible = ref(false)
const replaceDialogVisible = ref(false)
const replaceTargetFile = ref<FileItem | null>(null)
// 重命名相关
const renameDialogVisible = ref(false)
const renameForm = reactive({
id: '',
newName: ''
})
// 创建文件夹相关
const createFolderDialogVisible = ref(false)
const createFolderLoading = ref(false)
const createFolderFormRef = ref()
const createFolderForm = reactive({
folderName: ''
})
// 表单验证规则
const createFolderRules = {
folderName: [
{ required: true, message: t('vabI18n.knowledge.document.createFolderDialog.rules.nameRequired', '文件夹名称不能为空'), trigger: 'blur' },
{ max: 50, message: t('vabI18n.knowledge.document.createFolderDialog.rules.nameMaxLength', '文件夹名称不能超过50个字符'), trigger: 'blur' },
{ pattern: /^[^<>:"/\\|?*]+$/, message: t('vabI18n.knowledge.document.createFolderDialog.rules.namePattern', '文件夹名称不能包含特殊字符'), trigger: 'blur' }
]
}
const segmentList= ref([])
// 右键菜单相关
const contextMenuVisible = ref(false)
const contextMenuPosition = reactive({
x: 0,
y: 0
})
// 面包屑导航相关
const currentParentId = ref<number | undefined>(undefined)
const breadcrumbPath = ref<Array<{id: number, name: string}>>([])
// 面包屑导航项接口
interface BreadcrumbItem {
id: number;
name: string;
}
const handleClose = () => {
uploadDialogVisible.value = false
segmentList.value = []
}
// 组件卸载时清理
onUnmounted(() => {
// 清理右键菜单事件监听器
document.removeEventListener('click', hideContextMenu)
// 停止自动刷新定时器
stopAutoRefresh()
})
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
// 新增字段适配TDatasetFiles
type: string // file-文件, folder-文件夹
parentId?: number
path: string
mimeType?: string
difyDocId?: string
difyStoragePath?: string
sourceUrl?: string // 新增文档源URL用于下载和预览
}
// 搜索关键词
const searchKeyword = ref('')
// 分页数据
const pagination = reactive({
current: 1,
size: 20,
total: 0
})
// 排序数据
const sortConfig = reactive({
orderBy: 'name',
orderDirection: 'ASC'
})
// 文件列表数据
const fileList = ref<FileItem[]>([])
//预览右抽屉状态
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 handleClose = () => {
previewDrawerVisible.value = false,
segmentList.value = []
}
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(parseInt(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({
fileId: parseInt(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 formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 处理取消上传
// 监听预览抽屉关闭事件,清理预览内容
watch(previewDrawerVisible, (newVal) => {
if (!newVal) {
previewTextContent.value = ''
previewMarkdownContent.value = ''
previewFileUrl.value = ''
previewFileType.value = ''
}
})
import { useUserStore } from '@/store/modules/user'
import { useAclStore } from '@/store/modules/acl'
import vab from '~/library/plugins/vab'
// 事件处理函数
const renderedHandler = () => {
previewLoading.value = false
console.log("渲染完成")
}
const errorHandler = () => {
previewLoading.value = false
console.log("渲染失败")
}
const triggerFileInput = () => {
uploadDialogVisible.value = true
}
// 文件替换处理 - 从行操作触发
const handleFileReplaceFromRow = (row: FileItem) => {
replaceTargetFile.value = row
replaceDialogVisible.value = true
}
const indexMethod = (index: number) => {
return (pagination.current - 1) * pagination.size + index + 1
}
// 处理DocUpload组件的上传成功回调
const handleUploadSuccess = async () => {
uploadDialogVisible.value = false
await fetchDocuments()
// ElNotification({
// title: t('vabI18n.knowledge.document.messages.uploadSuccess'),
// message: t('vabI18n.knowledge.document.messages.uploadSuccessEnd'),
// type: 'success'
// })
}
// 处理文件替换成功回调
const handleReplaceSuccess = async () => {
replaceDialogVisible.value = false
replaceTargetFile.value = null
selectedRows.value = [] // 清空选择
await fetchDocuments()
ElNotification({
title: '替换成功',
message: '文件替换成功',
type: 'success'
})
}
//文件下载
const handleDownload = async (row: FileItem) => {
try {
// 使用新的API下载方式传入fileId数字类型
const response = await downloadDocument(parseInt(row.id))
// 创建 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 (fileId: number) => {
const response = await previewDocumentUrl(fileId)
console.log("response",response)
return response.data
}
// 获取文件文本内容用于txt和markdown预览
const fetchFileContent = async (fileId: number, fileType: string) => {
try {
// 对于txt和markdown文件我们需要获取原始文本内容
// 这里可以通过下载接口获取文件内容,然后转为文本
const response = await downloadDocument(fileId)
// 将响应数据转为文本
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
previewFileType.value = row.fileType;
try {
// 对于txt和markdown文件获取文本内容
if (['txt', 'md', 'markdown', 'mdx'].includes(row.fileType.toLowerCase())) {
const content = await fetchFileContent(parseInt(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 {
// 优先使用sourceUrl进行预览
if (row.sourceUrl) {
previewFileUrl.value = row.sourceUrl
console.log("previewFileUrl (from sourceUrl)", previewFileUrl.value)
} else {
// 兜底使用新的API获取预览URL
previewFileUrl.value = await fetchPreviewUrl(parseInt(row.id))
console.log("previewFileUrl (from API)", previewFileUrl.value)
}
}
const resp = await getSegmentList({
datasetId:datasetId.value,
documentId: row.difyDocId
})
if (resp.data?.length) {
segmentList.value = resp.data
}
} 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)
// 定时器相关
const refreshTimer = ref<NodeJS.Timeout | null>(null)
// 启动定时器
const startAutoRefresh = () => {
stopAutoRefresh() // 先清除可能存在的定时器
refreshTimer.value = setInterval(() => {
// 只有在非加载状态下才自动刷新
if (!tableLoading.value) {
fetchDocuments()
}
}, 30000) // 5秒刷新一次
}
// 停止定时器
const stopAutoRefresh = () => {
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
refreshTimer.value = null
}
}
// 修改fetchDocuments函数
const fetchDocuments = async () => {
tableLoading.value = true
try {
const params = {
difyDatasetId: datasetId.value,
parentId: currentParentId.value, // 使用当前文件夹ID
fileName: searchKeyword.value || undefined,
orderBy: sortConfig.orderBy,
orderDirection: sortConfig.orderDirection,
pageNo: pagination.current,
pageSize: pagination.size
}
const { data } = await getDatasetDocPage(params)
// 映射API数据到表格结构适配新的TDatasetFiles字段
fileList.value = data.content.map((item: any) => ({
id: item.id,
fileName: item.name,
parseStatus: formatIndexStatus(item.indexingStatus),
fileType: item.extension?.replace('.', '').toLowerCase() || item.name.split('.').pop()?.toLowerCase() || '',
charCount: item.size || 0, // 使用文件大小,如果有字符数可以后续添加
createDate: formatTimestamp(Math.floor(new Date(item.createdAt).getTime() / 1000)),
status: item.indexingStatus,
// 新增字段
type: item.type,
parentId: item.parentId,
path: item.path,
mimeType: item.mimeType,
difyDocId: item.difyDocId,
difyStoragePath: item.difyStoragePath,
sourceUrl: item.sourceUrl // 新增映射sourceUrl字段
}))
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 handleGoBack = () => {
router.push('/datasets')
}
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> = {
'preprocessing': '预处理',
'indexing': '解析中',
'waiting': '等待中',
'queued': '排队中',
'failed': '解析失败',
'completed': '解析成功'
}
return statusMap[status] || '未知状态'
}
// 获取状态对应的标签类型
const getStatusTagType = (status: string): 'success' | 'primary' | 'warning' | 'info' | 'danger' => {
const typeMap: Record<string, 'success' | 'primary' | 'warning' | 'info' | 'danger'> = {
'preprocessing': 'info',
'indexing': 'warning',
'waiting': 'info',
'queued': 'info', // 修改为info类型显示较深灰色
'failed': 'danger',
'completed': 'success'
}
return typeMap[status] || 'info'
}
// 获取状态对应的图标
const getStatusIcon = (status: string) => {
const iconMap: Record<string, any> = {
'preprocessing': Loading,
'indexing': Loading,
'waiting': Loading,
'queued': Loading,
'failed': Close,
'completed': Check
}
return iconMap[status] || InfoFilled
}
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(parseInt(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()
// 启动自动刷新定时器
startAutoRefresh()
})
// 分页变化处理
const handlePaginationChange = () => {
fetchDocuments()
}
// 搜索处理
const handleSearch = () => {
pagination.current = 1
fetchDocuments()
}
// 排序变化处理
const handleSortChange = (sortInfo: any) => {
if (sortInfo.prop && sortInfo.order) {
// 映射前端字段到后端字段
const fieldMapping: Record<string, string> = {
'fileName': 'name',
'createDate': 'created_at',
'charCount': 'size'
}
sortConfig.orderBy = fieldMapping[sortInfo.prop] || sortInfo.prop
sortConfig.orderDirection = sortInfo.order === 'ascending' ? 'ASC' : 'DESC'
// 重置到第一页并刷新数据
pagination.current = 1
fetchDocuments()
}
}
// 处理文件夹打开
const handleOpenFolder = (row: FileItem) => {
// 设置当前文件夹ID并更新面包屑路径
currentParentId.value = parseInt(row.id)
breadcrumbPath.value.push({
id: parseInt(row.id),
name: row.fileName
})
// 重置分页到第一页
pagination.current = 1
// 刷新文件列表
fetchDocuments()
}
// 表格行双击处理
const handleRowDoubleClick = (row: FileItem) => {
if (row.type === 'folder') {
handleOpenFolder(row)
} else if (row.type === 'file' && row.status === 'completed') {
// 如果是文件且状态为已完成,则预览
handlePreview(row)
}
}
// 面包屑导航处理
const navigateToRoot = () => {
currentParentId.value = undefined
breadcrumbPath.value = []
pagination.current = 1
fetchDocuments()
}
const navigateToFolder = (folder: BreadcrumbItem, index: number) => {
// 如果点击的不是最后一个面包屑项,则导航到该文件夹
if (index < breadcrumbPath.value.length - 1) {
currentParentId.value = folder.id
breadcrumbPath.value = breadcrumbPath.value.slice(0, index + 1)
pagination.current = 1
fetchDocuments()
}
}
// 右键菜单处理
const handleRightClick = (event: MouseEvent) => {
// 只在空白区域右键时显示菜单
const target = event.target as HTMLElement
if (target.closest('.el-table') || target.closest('.action-bar')) {
return
}
contextMenuPosition.x = event.clientX
contextMenuPosition.y = event.clientY
contextMenuVisible.value = true
// 监听点击事件来隐藏菜单
document.addEventListener('click', hideContextMenu)
}
const hideContextMenu = () => {
contextMenuVisible.value = false
document.removeEventListener('click', hideContextMenu)
}
// 从右键菜单创建文件夹
const handleCreateFolderFromContext = () => {
hideContextMenu()
createFolderForm.folderName = ''
createFolderDialogVisible.value = true
}
// 创建文件夹确认
const handleCreateFolderConfirm = async () => {
if (!createFolderFormRef.value) return
try {
await createFolderFormRef.value.validate()
} catch {
return
}
createFolderLoading.value = true
try {
const folderData: CreateFolderReq = {
datasetId: datasetId.value,
folderName: createFolderForm.folderName,
parentId: currentParentId.value, // 在当前目录下创建文件夹
ownerId: aclStore.getUserId || 1 // 从acl store获取用户ID
}
await createFolder(folderData)
ElNotification({
title: t('vabI18n.knowledge.document.messages.createFolderSuccess', '创建成功'),
message: t('vabI18n.knowledge.document.messages.createFolderSuccessMessage', `文件夹"${createFolderForm.folderName}"创建成功`),
type: 'success'
})
createFolderDialogVisible.value = false
createFolderForm.folderName = ''
// 刷新文件列表
await fetchDocuments()
} catch (error) {
console.error('创建文件夹失败:', error)
ElNotification({
title: t('vabI18n.knowledge.document.errors.createFolderFailed', '创建失败'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.createFolderFailed', '创建文件夹失败'),
type: 'error'
})
} finally {
createFolderLoading.value = false
}
}
</script>
<style lang="scss" scoped>
.main-container {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.page-header {
margin-bottom: 24px;
.header-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.back-button {
color: #409eff;
font-size: 14px;
padding: 8px 12px;
&:hover {
background-color: #ecf5ff;
color: #337ecc;
}
}
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin: 0;
}
.page-description {
font-size: 14px;
color: #909399;
margin: 0;
}
.breadcrumb-container {
margin: 12px 0;
.breadcrumb-clickable {
cursor: pointer;
&:hover {
color: #409eff;
}
}
::v-deep .el-breadcrumb__inner {
display: flex;
align-items: center;
gap: 4px;
&.is-link:hover {
color: #409eff;
}
}
::v-deep .el-breadcrumb__item:last-child .el-breadcrumb__inner {
color: #303133;
font-weight: 500;
}
}
}
// 内容布局样式
.content-layout {
display: flex;
gap: 20px;
position: relative;
transition: all 0.3s ease;
}
.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); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
60% { transform: translateY(-5px); }
}
.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;
}
.folder-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.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:not(.disabled-btn) {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
&.disabled-btn {
color: #c0c4cc !important;
cursor: not-allowed !important;
background: transparent !important;
border-color: transparent !important;
&:hover {
transform: none !important;
box-shadow: none !important;
color: #c0c4cc !important;
background: transparent !important;
}
}
}
}
.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 {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
::v-deep .el-dialog__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0;
padding: 24px 32px;
}
::v-deep .el-dialog__title {
color: white;
font-weight: 600;
font-size: 20px;
}
::v-deep .el-dialog__close {
color: white;
font-size: 20px;
&:hover {
color: rgba(255, 255, 255, 0.8);
transform: scale(1.1);
}
}
::v-deep .el-dialog__body {
padding: 32px;
background: #fafbfc;
}
::v-deep .el-dialog__footer {
padding: 20px 32px 32px;
background: #fafbfc;
border-top: 1px solid #e9ecef;
}
}
// 重命名对话框样式
.rename-dialog {
::v-deep .el-dialog {
border-radius: 20px;
overflow: hidden;
box-shadow: 0 25px 80px rgba(64, 158, 255, 0.15);
backdrop-filter: blur(10px);
}
::v-deep .el-dialog__header {
background: linear-gradient(135deg, #409eff 0%, #36cfc9 100%);
color: white;
border-radius: 0;
padding: 24px 32px;
text-align: center;
}
::v-deep .el-dialog__title {
color: white;
font-weight: 700;
font-size: 20px;
letter-spacing: 0.5px;
}
::v-deep .el-dialog__close {
color: white;
font-size: 20px;
&:hover {
color: rgba(255, 255, 255, 0.8);
transform: scale(1.1) rotate(90deg);
transition: all 0.3s ease;
}
}
::v-deep .el-dialog__body {
padding: 40px 32px 32px;
background: linear-gradient(135deg, #f8fbff 0%, #f0f9ff 100%);
}
.dialog-content {
text-align: center;
.dialog-icon {
margin-bottom: 24px;
animation: pulse 2s infinite;
}
.rename-form {
.form-label {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
text-align: left;
}
.rename-input {
::v-deep .el-input__wrapper {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
border: 2px solid transparent;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.15);
}
&.is-focus {
border-color: #409eff;
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.2);
}
}
}
}
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 16px;
padding: 20px 0;
background: linear-gradient(135deg, #f8fbff 0%, #f0f9ff 100%);
.el-button {
border-radius: 12px;
padding: 12px 24px;
font-weight: 600;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
&.el-button--primary {
background: linear-gradient(135deg, #409eff 0%, #36cfc9 100%);
border: none;
&:hover {
background: linear-gradient(135deg, #337ecc 0%, #2bb0a8 100%);
}
}
}
}
}
// 创建文件夹对话框样式
.create-folder-dialog {
::v-deep .el-dialog {
border-radius: 20px;
overflow: hidden;
box-shadow: 0 25px 80px rgba(103, 194, 58, 0.15);
backdrop-filter: blur(10px);
}
::v-deep .el-dialog__header {
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
color: white;
border-radius: 0;
padding: 24px 32px;
text-align: center;
}
::v-deep .el-dialog__title {
color: white;
font-weight: 700;
font-size: 20px;
letter-spacing: 0.5px;
}
::v-deep .el-dialog__close {
color: white;
font-size: 20px;
&:hover {
color: rgba(255, 255, 255, 0.8);
transform: scale(1.1) rotate(90deg);
transition: all 0.3s ease;
}
}
::v-deep .el-dialog__body {
padding: 40px 32px 32px;
background: linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%);
}
.dialog-content {
text-align: center;
.dialog-icon {
margin-bottom: 24px;
animation: bounce 2s infinite;
}
.create-folder-form {
.form-label {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
text-align: left;
}
.folder-name-input {
::v-deep .el-input__wrapper {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(103, 194, 58, 0.1);
border: 2px solid transparent;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 6px 16px rgba(103, 194, 58, 0.15);
}
&.is-focus {
border-color: #67c23a;
box-shadow: 0 8px 20px rgba(103, 194, 58, 0.2);
}
}
}
}
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 16px;
padding: 20px 0;
background: linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%);
.el-button {
border-radius: 12px;
padding: 12px 24px;
font-weight: 600;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
&.el-button--primary {
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
border: none;
&:hover {
background: linear-gradient(135deg, #529e2b 0%, #6cb33f 100%);
}
}
}
}
}
// 文件替换信息样式
.replace-info {
margin-bottom: 24px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #e6a23c;
.target-file-info {
h4 {
margin: 0 0 16px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.file-info-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid #ebeef5;
.file-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
object-fit: contain;
}
.file-details {
flex: 1;
.file-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.file-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #909399;
span {
&:not(:last-child)::after {
content: '•';
margin-left: 8px;
color: #dcdfe6;
}
}
}
}
}
}
}
// 预览抽屉样式
.preview-drawer {
::v-deep .el-drawer__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-bottom: none;
margin-bottom: 0;
padding: 24px 32px;
color: white;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.15);
.el-drawer__title {
font-weight: 700;
color: white;
font-size: 18px;
letter-spacing: 0.5px;
}
.el-drawer__close-btn {
color: white;
font-size: 20px;
&:hover {
color: rgba(255, 255, 255, 0.8);
transform: scale(1.1);
}
}
}
::v-deep .el-drawer__body {
background: #fafbfc;
padding: 0;
}
}
// 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;
}
&.rotating-tag .el-icon.is-loading {
animation: spin 1s linear infinite;
}
// 不同状态的标签样式
&[type="info"] {
background-color: #f4f4f5;
border-color: #d3d4d6;
color: #909399;
}
&[type="warning"] {
background-color: #fdf6ec;
border-color: #f5dab1;
color: #e6a23c;
}
&[type="success"] {
background-color: #f0f9ff;
border-color: #b3d8ff;
color: #67c23a;
}
&[type="danger"] {
background-color: #fef0f0;
border-color: #fbc4c4;
color: #f56c6c;
}
}
// 深度解析对话框样式
.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;
}
}
// 右键菜单样式
.context-menu {
position: fixed;
background: white;
border: none;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
padding: 0;
z-index: 3000;
min-width: 240px;
backdrop-filter: blur(20px);
overflow: hidden;
.context-menu-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px 12px;
border-bottom: none;
.menu-title {
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
}
}
.context-menu-item {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 16px 20px;
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 1px solid #f0f2f5;
&:last-child {
border-bottom: none;
}
&:hover {
background: linear-gradient(135deg, #f8fbff 0%, #e8f4fd 100%);
transform: translateX(4px);
}
.item-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: linear-gradient(135deg, #409eff 0%, #36cfc9 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
flex-shrink: 0;
transition: all 0.3s ease;
}
.item-content {
flex: 1;
padding-top: 2px;
.item-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
line-height: 1.4;
}
.item-desc {
font-size: 12px;
color: #909399;
line-height: 1.3;
}
}
&:hover .item-icon {
transform: scale(1.1);
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.3);
}
}
}
// 右键菜单动画
.context-menu-fade-enter-active {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.context-menu-fade-leave-active {
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.context-menu-fade-enter-from {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
.context-menu-fade-leave-to {
opacity: 0;
transform: scale(0.9) translateY(-5px);
}
// 响应式设计
@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;
}
}
.segment-list-container {
width: 30%;
border-right: 1px solid #ebeef5;
overflow-y: auto;
padding: 16px;
background: #f9fafc;
display: flex;
flex-direction: column;
gap: 12px;
h3 {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-form-item {
cursor: pointer;
padding: 16px; // 内边距增加
border-radius: 8px;
background: white;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
min-height: 100px; // 增加最小高度,让卡片更高
display: flex;
flex-direction: column;
justify-content: center; // 让内容垂直居中
&:hover {
background: #f0f4ff;
transform: translateX(2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
span {
font-size: 14px;
font-weight: 500;
color: #606266;
margin-top: 4px;
word-break: break-word; // 防止长文本溢出
}
h4 {
font-family: "Microsoft YaHei", "微软雅黑", sans-serif; // 设置微软雅黑字体
font-size: 14px;
font-weight: 600;
color: #4a4a4a;
margin: 0;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
}
.empty-state {
text-align: center;
color: #909399;
font-size: 14px;
margin-top: 20px;
}
}
/* 右侧预览内容保持原有样式 */
.preview-content-container {
flex: 1;
padding: 10px;
overflow-y: auto;
background: #ffffff;
border-radius: 0 8px 8px 0;
}
</style>