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

1972 lines
55 KiB
Vue
Raw Normal View History

2025-07-18 16:38:18 +08:00
<template>
<div class="main-container">
<div class="page-header">
<h2 class="page-title">{{ datasetName }}</h2>
<div class="page-description">
{{t('vabI18n.knowledge.document.header.description')}} {{ pagination.total }} {{ $t(pagination.total > 1
? 'vabI18n.knowledge.document.header.descriptionEnds'
: 'vabI18n.knowledge.document.header.descriptionEnd') }}
</div>
</div>
2025-07-30 10:02:44 +08:00
<!-- 主要内容区域 -->
<div class="content-layout" :class="{ 'has-sidebar': showTaskSidebar }">
<div class="file-manager-container">
2025-07-18 16:38:18 +08:00
<!-- 操作栏 -->
<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>
2025-07-30 10:02:44 +08:00
<!-- 任务监控切换按钮 -->
<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>
2025-07-18 16:38:18 +08:00
<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>
2025-07-30 10:02:44 +08:00
<!-- 右侧任务面板 -->
<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>
2025-07-18 16:38:18 +08:00
<!-- 上传文件对话框 -->
<el-dialog v-model="uploadDialogVisible" :title="t('vabI18n.knowledge.document.buttons.upload')" width="600px" class="upload-dialog">
<el-form :model="uploadForm" label-width="120px" class="upload-form">
2025-07-21 17:39:30 +08:00
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.indexingTechnique')" label-width="auto">
2025-07-18 16:38:18 +08:00
<el-radio-group v-model="uploadForm.indexingTechnique">
<el-radio value="high_quality">{{t('vabI18n.knowledge.document.uploadDialog.indexingOptions.highQuality')}}</el-radio>
<el-radio value="economy">{{t('vabI18n.knowledge.document.uploadDialog.indexingOptions.economy')}}</el-radio>
</el-radio-group>
</el-form-item>
2025-07-21 17:39:30 +08:00
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.preProcessingRules')" label-width="auto">
2025-07-18 16:38:18 +08:00
<el-checkbox-group v-model="uploadForm.preProcessingRules">
<el-checkbox value="remove_extra_spaces">{{t('vabI18n.knowledge.document.uploadDialog.ruleOptions.removeSpaces')}}</el-checkbox>
<el-checkbox value="remove_urls_emails">{{t('vabI18n.knowledge.document.uploadDialog.ruleOptions.removeUrls')}}</el-checkbox>
</el-checkbox-group>
</el-form-item>
2025-07-21 17:39:30 +08:00
<el-form-item :label="t('vabI18n.knowledge.document.uploadDialog.segmentation')" label-width="auto">
2025-07-18 16:38:18 +08:00
<div class="segment-rule">
<el-input
v-model="uploadForm.segmentSeparator"
:placeholder="t('vabI18n.knowledge.document.uploadDialog.ruleOptions.segmentSeparator')"
style="width: 200px; margin-right: 15px;"
/>
<el-input-number
v-model="uploadForm.segmentMaxTokens"
:min="100"
:max="2000"
:placeholder="t('vabI18n.knowledge.document.uploadDialog.maxTokens')"
/>
</div>
</el-form-item>
2025-07-29 15:53:14 +08:00
<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>
2025-07-18 16:38:18 +08:00
<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>
2025-07-29 15:53:14 +08:00
2025-07-30 10:02:44 +08:00
2025-07-18 16:38:18 +08:00
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
2025-07-30 10:02:44 +08:00
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
2025-07-18 16:38:18 +08:00
import VueOfficePdf from '@vue-office/pdf'
2025-07-29 15:53:14 +08:00
import { getDatasetDocPage, uploadDocument, deleteDocument, downloadDocument, previewDocumentUrl, renameDocument, getDeepAnalysisList } from '@/api/dataset'
2025-07-18 16:38:18 +08:00
//引入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,
2025-07-30 10:02:44 +08:00
Loading,
DataAnalysis,
Document
2025-07-18 16:38:18 +08:00
} 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: '###',
2025-07-29 15:53:14 +08:00
segmentMaxTokens: 500,
deepAnalysis: false
2025-07-18 16:38:18 +08:00
})
// 重命名相关
const renameDialogVisible = ref(false)
const renameForm = reactive({
id: '',
newName: ''
})
2025-07-29 15:53:14 +08:00
// 深度解析相关
const deepAnalysisLoading = ref(false)
const deepAnalysisList = ref<PdfTask[]>([])
2025-07-30 10:02:44 +08:00
// 实时进度监控相关
const progressPollingTimer = ref<NodeJS.Timeout | null>(null)
const showTaskSidebar = ref(false)
const POLLING_INTERVAL = 3000 // 3秒轮询一次
2025-07-29 15:53:14 +08:00
// 定义深度解析任务类型
interface PdfTask {
name: string
taskId: string
percent: number
datasetName: string
createTime: number
2025-07-30 10:02:44 +08:00
}
// 计算属性
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
2025-07-29 15:53:14 +08:00
2025-07-30 10:02:44 +08:00
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')
2025-07-29 15:53:14 +08:00
}
2025-07-30 10:02:44 +08:00
// 轮询控制
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()
})
2025-07-18 16:38:18 +08:00
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 ''
}
}
2025-07-18 16:38:18 +08:00
const iconMap: Record<string, string> = {
'pdf': getIconUrl('pdf'),
'doc': getIconUrl('word'),
'docx': getIconUrl('word'),
'xls': getIconUrl('excel'),
'xlsx': getIconUrl('excel'),
'ppt': getIconUrl('ppt'),
'pptx': getIconUrl('ppt'),
'txt': getIconUrl('txt'),
'md': getIconUrl('txt'),
'markdown': getIconUrl('txt'),
'mdx': getIconUrl('txt'),
'html': getIconUrl('txt'),
'htm': getIconUrl('txt'),
'csv': getIconUrl('excel'),
'vtt': getIconUrl('txt'),
'properties': getIconUrl('txt'),
'xml': getIconUrl('xml'),
'zip': getIconUrl('zip'),
'mp4': getIconUrl('video'),
'avi': getIconUrl('video'),
'mp3': getIconUrl('audio'),
'wav': getIconUrl('audio'),
'jpg': getIconUrl('img'),
'jpeg': getIconUrl('img'),
'png': getIconUrl('img'),
'gif': getIconUrl('img'),
}
return iconMap[fileType.toLowerCase()] || getIconUrl('unknown')
}
// 多选相关
const selectedRows = ref<FileItem[]>([])
interface FileItem {
id: string
fileName: string
fileType: string
parseStatus: string
previewConvert: string
size: string
charCount: number
createDate: string
status: string
}
// 搜索关键词
const searchKeyword = ref('')
// 分页数据
const pagination = reactive({
current: 1,
size: 20,
total: 0
})
// 文件列表数据
const fileList = ref<FileItem[]>([])
const fileInput = ref<HTMLInputElement>()
//预览右抽屉状态
const previewDrawerVisible = ref(false)
const previewFileType = ref('')
const previewFileUrl = ref('')
// 添加文本内容预览状态
const previewTextContent = ref('')
const previewMarkdownContent = ref('')
// 选择变化处理
const handleSelectionChange = (selection: FileItem[]) => {
selectedRows.value = selection
}
// 批量删除处理
const handleBatchDelete = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning(t('vabI18n.knowledge.document.messages.noFileSelected'))
return
}
try {
await ElMessageBox.confirm(
t('vabI18n.knowledge.document.messages.batchDeleteConfirm', {
count: selectedRows.value.length
}),
t('vabI18n.knowledge.document.messages.deleteConfirmTitle'),
{
confirmButtonText: t('vabI18n.knowledge.document.buttons.confirm'),
cancelButtonText: t('vabI18n.knowledge.document.buttons.cancel'),
type: 'warning'
}
)
const loading = ElLoading.service({
lock: true,
text: t('vabI18n.knowledge.document.messages.deleteing'),
background: 'rgba(0, 0, 0, 0.7)'
})
try {
// 批量删除
for (const row of selectedRows.value) {
await deleteDocument({
datasetId: datasetId.value,
documentId: row.id
})
}
ElNotification({
title: t('vabI18n.knowledge.document.messages.deleteSuccessOk'),
message: t('vabI18n.knowledge.document.messages.batchDeleteSuccess') + ` ${selectedRows.value.length} ` + t('vabI18n.knowledge.document.messages.batchDeleteSuccessEnd'),
type: 'success'
})
// 清空选择
selectedRows.value = []
// 刷新文件列表
await fetchDocuments()
} finally {
loading.close()
}
} catch (error) {
console.error('批量删除失败:', error)
ElNotification({
title: t('vabI18n.knowledge.document.errors.deleteFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.deleteFailed'),
type: 'error'
})
}
}
const originalExtension = ref('')
// 重命名处理
const handleRename = (row: FileItem) => {
renameForm.id = row.id
const parts = row.fileName.split('.')
if (parts.length > 1) {
originalExtension.value = parts.pop()!
renameForm.newName = parts.join('.')
} else {
originalExtension.value = ''
renameForm.newName = row.fileName
}
renameDialogVisible.value = true
}
// 重命名确认
const handleRenameConfirm = async () => {
const newName = (originalExtension.value
? renameForm.newName.trim() + '.' + originalExtension.value
: renameForm.newName.trim())
if (!newName || newName === '.') {
ElMessage.warning(t('vabI18n.knowledge.document.messages.FilenamecantEmpty'))
return
}
try {
await renameDocument({
documentId: renameForm.id,
newName: newName
})
ElNotification({
title: t('vabI18n.knowledge.document.errors.renameSuccess'),
message: t('vabI18n.knowledge.document.messages.renameSuccess'),
type: 'success'
})
renameDialogVisible.value = false
await fetchDocuments()
} catch (error) {
ElNotification({
title: t('vabI18n.knowledge.document.errors.renameFailed'),
message: t('vabI18n.knowledge.document.messages.renameFailed'),
type: 'error'
})
}
}
// 数字格式化
const formatNumber = (num: number) => {
return num.toLocaleString()
}
// 处理取消上传
const handleCancelUpload = () => {
uploadDialogVisible.value = false
uploadFiles.value = []
uploadRef.value?.clearFiles()
}
// 监听对话框关闭事件
watch(uploadDialogVisible, (newVal) => {
if (!newVal) {
uploadFiles.value = []
uploadRef.value?.clearFiles()
}
})
// 监听预览抽屉关闭事件,清理预览内容
watch(previewDrawerVisible, (newVal) => {
if (!newVal) {
previewTextContent.value = ''
previewMarkdownContent.value = ''
previewFileUrl.value = ''
previewFileType.value = ''
}
})
import { useUserStore } from '@/store/modules/user'
import vab from '~/library/plugins/vab'
// 事件处理函数
const renderedHandler = () => {
previewLoading.value = false
console.log("渲染完成")
}
const errorHandler = () => {
previewLoading.value = false
console.log("渲染失败")
}
const triggerFileInput = () => {
fileInput.value?.click()
uploadDialogVisible.value = true
}
const indexMethod = (index: number) => {
return (pagination.current - 1) * pagination.size + index + 1
}
// 文件选择处理
const handleFileChange = (file: ElUploadFile, files: UploadFiles) => {
uploadFiles.value = files
}
// 上传处理
const handleUpload = async () => {
if (uploadFiles.value.length === 0) {
ElMessage.warning(t('vabI18n.knowledge.document.messages.noUploadFile'))
return
}
const loading = ElLoading.service({
lock: true,
text: t('vabI18n.knowledge.document.messages.uploadLoading'),
background: 'rgba(0, 0, 0, 0.7)'
})
try {
// 构造请求数据
const processRule = {
rules: {
preProcessingRules: uploadForm.preProcessingRules.map(rule => ({
id: rule,
enabled: true
})),
segmentation: {
separator: uploadForm.segmentSeparator || '###',
maxTokens: uploadForm.segmentMaxTokens
}
},
mode: "custom"
}
// 遍历上传文件
for (const file of uploadFiles.value) {
const formData = new FormData()
formData.append('file', file.raw!)
formData.append('request', new Blob([JSON.stringify({
datasetId: datasetId.value,
indexingTechnique: uploadForm.indexingTechnique,
processRule: processRule,
2025-07-29 15:53:14 +08:00
deepAnalysis: uploadForm.deepAnalysis
2025-07-18 16:38:18 +08:00
})], {
type: 'application/json'
}))
await uploadDocument(formData)
}
ElNotification({
title: t('vabI18n.knowledge.document.messages.uploadSuccess'),
message: `${uploadFiles.value.length} ${t('vabI18n.knowledge.document.messages.uploadSuccessEnd')}`,
type: 'success'
})
uploadDialogVisible.value = false
await fetchDocuments()
2025-07-30 10:02:44 +08:00
// 如果启用了深度解析,开始监控进度
if (uploadForm.deepAnalysis) {
setTimeout(async () => {
await fetchDeepAnalysisList()
}, 2000) // 2秒后获取任务列表
}
2025-07-18 16:38:18 +08:00
} 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()
}
2025-07-29 15:53:14 +08:00
const formatTimestampToLocaleString = (timestamp: number): string => {
const date = new Date(timestamp * 1000)
return date.toLocaleString()
}
2025-07-18 16:38:18 +08:00
// 工具函数
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'
})
}
}
2025-07-30 10:02:44 +08:00
onMounted(async () => {
2025-07-18 16:38:18 +08:00
datasetId.value = route.params.id as string
datasetName.value = route.query.name as string
// 这里可以调用API获取详细数据
2025-07-30 10:02:44 +08:00
await fetchDocuments()
// 初始化时获取深度解析任务列表
await fetchDeepAnalysisList(false)
2025-07-18 16:38:18 +08:00
})
// 分页变化处理
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;
}
}
2025-07-30 10:02:44 +08:00
// 内容布局样式
.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;
}
2025-07-18 16:38:18 +08:00
.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;
2025-07-30 10:02:44 +08:00
// 深度解析按钮样式
.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;
}
}
}
2025-07-18 16:38:18 +08:00
}
.batch-delete-btn {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
2025-07-30 10:02:44 +08:00
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
2025-07-18 16:38:18 +08:00
.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;
}
}
2025-07-29 15:53:14 +08:00
// 深度解析对话框样式
.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;
}
}
2025-07-30 10:02:44 +08:00
// 任务统计样式
.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;
}
}
}
2025-07-29 15:53:14 +08:00
.empty-state {
padding: 40px 20px;
text-align: center;
}
.task-list {
2025-07-30 10:02:44 +08:00
max-height: 500px;
2025-07-29 15:53:14 +08:00
overflow-y: auto;
padding: 10px 0;
}
.task-item {
border: 1px solid #ebeef5;
2025-07-30 10:02:44 +08:00
border-radius: 12px;
margin-bottom: 16px;
padding: 20px;
2025-07-29 15:53:14 +08:00
background: #fafafa;
transition: all 0.3s ease;
&:hover {
2025-07-30 10:02:44 +08:00
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;
2025-07-29 15:53:14 +08:00
}
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
2025-07-30 10:02:44 +08:00
gap: 24px;
2025-07-29 15:53:14 +08:00
}
.task-info {
flex: 1;
.task-name {
2025-07-30 10:02:44 +08:00
margin: 0 0 12px 0;
2025-07-29 15:53:14 +08:00
font-size: 16px;
font-weight: 600;
color: #303133;
word-break: break-all;
}
.task-dataset, .task-id, .task-time {
2025-07-30 10:02:44 +08:00
margin: 6px 0;
font-size: 13px;
2025-07-29 15:53:14 +08:00
color: #909399;
}
.task-dataset {
color: #67c23a;
font-weight: 500;
}
}
.task-progress {
2025-07-30 10:02:44 +08:00
flex: 0 0 220px;
.progress-status {
margin-top: 8px;
text-align: center;
.status-tag {
font-weight: 600;
}
}
2025-07-29 15:53:14 +08:00
::v-deep .el-progress__text {
font-weight: 600;
}
}
2025-07-18 16:38:18 +08:00
// 响应式设计
@media (max-width: 768px) {
.main-container {
padding: 12px;
}
2025-07-30 10:02:44 +08:00
.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;
}
}
}
}
2025-07-18 16:38:18 +08:00
.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>