1972 lines
55 KiB
Vue
1972 lines
55 KiB
Vue
<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> |