2466 lines
62 KiB
Vue
2466 lines
62 KiB
Vue
<template>
|
||
<div class="main-container">
|
||
<div class="page-header">
|
||
<div class="header-top">
|
||
<el-button
|
||
type="text"
|
||
:icon="ArrowLeft"
|
||
@click="handleGoBack"
|
||
class="back-button"
|
||
>
|
||
{{t('vabI18n.knowledge.document.buttons.back')}}
|
||
</el-button>
|
||
<h2 class="page-title">{{ datasetName }}</h2>
|
||
</div>
|
||
|
||
<!-- 面包屑导航 -->
|
||
<div class="breadcrumb-container">
|
||
<el-breadcrumb separator="/">
|
||
<el-breadcrumb-item @click="navigateToRoot" class="breadcrumb-clickable">
|
||
<el-icon><House /></el-icon>
|
||
<span>{{t('vabI18n.knowledge.document.breadcrumb.root', '根目录')}}</span>
|
||
</el-breadcrumb-item>
|
||
<el-breadcrumb-item
|
||
v-for="(folder, index) in breadcrumbPath"
|
||
:key="folder.id"
|
||
@click="navigateToFolder(folder, index)"
|
||
:class="{ 'breadcrumb-clickable': index < breadcrumbPath.length - 1 }"
|
||
>
|
||
<span>{{ folder.name }}</span>
|
||
</el-breadcrumb-item>
|
||
</el-breadcrumb>
|
||
</div>
|
||
|
||
<div class="page-description">
|
||
{{t('vabI18n.knowledge.document.header.description')}} {{ pagination.total }} {{ $t(pagination.total > 1
|
||
? 'vabI18n.knowledge.document.header.descriptionEnds'
|
||
: 'vabI18n.knowledge.document.header.descriptionEnd') }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="content-layout">
|
||
<div
|
||
class="file-manager-container"
|
||
@contextmenu.prevent="handleRightClick"
|
||
@click="hideContextMenu"
|
||
>
|
||
<!-- 操作栏 -->
|
||
<div class="action-bar">
|
||
<div class="left-group">
|
||
<el-input
|
||
v-model="searchKeyword"
|
||
:placeholder="$t('vabI18n.knowledge.document.search.placeholder')"
|
||
class="search-input"
|
||
clearable
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-button type="primary" :icon="Search" @click="handleSearch">{{t('vabI18n.knowledge.document.buttons.search')}}</el-button>
|
||
</div>
|
||
|
||
<div class="right-group">
|
||
<el-button
|
||
v-if="selectedRows.length > 0"
|
||
type="danger"
|
||
:icon="Delete"
|
||
@click="handleBatchDelete"
|
||
class="batch-delete-btn"
|
||
>
|
||
{{t('vabI18n.knowledge.document.buttons.batchDelete', { count: selectedRows.length })}}
|
||
</el-button>
|
||
<el-button :icon="Refresh" @click="handleRefresh">{{t('vabI18n.knowledge.document.buttons.refresh')}}</el-button>
|
||
|
||
<el-button type="primary" :icon="Upload" @click="triggerFileInput">{{t('vabI18n.knowledge.document.buttons.upload')}}</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件表格 -->
|
||
<el-table
|
||
v-loading="tableLoading"
|
||
:element-loading-text="$t('vabI18n.knowledge.document.messages.loading')"
|
||
:data="fileList"
|
||
stripe
|
||
class="file-table"
|
||
:row-style="{ height: '50px' }"
|
||
:header-row-style="{ height: '50px' }"
|
||
@selection-change="handleSelectionChange"
|
||
@row-dblclick="handleRowDoubleClick"
|
||
@sort-change="handleSortChange"
|
||
>
|
||
<el-table-column type="selection" width="55" />
|
||
<el-table-column
|
||
:label="$t('vabI18n.knowledge.document.table.index')"
|
||
width="80"
|
||
align="center"
|
||
type="index"
|
||
:index="indexMethod"
|
||
/>
|
||
<el-table-column prop="fileName" :label="t('vabI18n.knowledge.document.table.fileName')" min-width="250" sortable="custom" :sort-orders="['ascending', 'descending']">
|
||
<template #default="{ row }">
|
||
<div class="file-name-cell">
|
||
<img
|
||
v-if="row.type === 'file'"
|
||
:src="getFileTypeIcon(row.fileType)"
|
||
alt="文件图标"
|
||
class="file-icon"
|
||
/>
|
||
<el-icon v-else class="folder-icon" size="20" color="#409eff">
|
||
<Folder />
|
||
</el-icon>
|
||
<span class="file-name" :title="row.fileName">{{ row.fileName }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="parseStatus" :label="t('vabI18n.knowledge.document.table.status')" width="120" align="center">
|
||
<template #default="{ row }">
|
||
<!-- 文件夹不显示状态 -->
|
||
<template v-if="row.type === 'file'">
|
||
<el-tag
|
||
:type="getStatusTagType(row.status)"
|
||
:class="{ 'rotating-tag': ['preprocessing', 'indexing', 'waiting', 'queued'].includes(row.status) }"
|
||
>
|
||
<el-icon
|
||
:class="{ 'is-loading': ['preprocessing', 'indexing', 'waiting', 'queued'].includes(row.status) }"
|
||
>
|
||
<component :is="getStatusIcon(row.status)" />
|
||
</el-icon>
|
||
{{ row.parseStatus }}
|
||
</el-tag>
|
||
</template>
|
||
<span v-else class="folder-status">--</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createDate" :label="t('vabI18n.knowledge.document.table.createDate')" width="180" sortable="custom" :sort-orders="['ascending', 'descending']" />
|
||
<el-table-column prop="charCount" :label="t('vabI18n.knowledge.document.table.charCount')" width="120" align="right" sortable="custom" :sort-orders="['ascending', 'descending']">
|
||
<template #default="{ row }">
|
||
<span class="char-count" v-if="row.type === 'file'">{{ formatFileSize(row.charCount) }}</span>
|
||
<span v-else class="folder-indicator">--</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="380" fixed="right">
|
||
<template #default="{ row }">
|
||
<div class="action-buttons">
|
||
<!-- 文件操作 -->
|
||
<template v-if="row.type === 'file'">
|
||
<el-button
|
||
:type="row.status === 'completed' ? 'primary' : 'info'"
|
||
:icon="View"
|
||
text
|
||
class="action-btn"
|
||
:disabled="row.status !== 'completed'"
|
||
:class="{ 'disabled-btn': row.status !== 'completed' }"
|
||
@click="row.status === 'completed' ? handlePreview(row) : null"
|
||
>
|
||
{{t('vabI18n.knowledge.document.buttons.preview')}}
|
||
</el-button>
|
||
<el-button
|
||
:type="row.status === 'completed' ? 'success' : 'info'"
|
||
:icon="Download"
|
||
text
|
||
class="action-btn"
|
||
:disabled="row.status !== 'completed'"
|
||
:class="{ 'disabled-btn': row.status !== 'completed' }"
|
||
@click="row.status === 'completed' ? handleDownload(row) : null"
|
||
>
|
||
{{t('vabI18n.knowledge.document.buttons.download')}}
|
||
</el-button>
|
||
<el-button
|
||
type="warning"
|
||
:icon="Upload"
|
||
text
|
||
class="action-btn"
|
||
@click="handleFileReplaceFromRow(row)"
|
||
>
|
||
替换
|
||
</el-button>
|
||
</template>
|
||
<!-- 文件夹操作 -->
|
||
<template v-else>
|
||
<el-button
|
||
type="primary"
|
||
:icon="View"
|
||
text
|
||
class="action-btn"
|
||
@click="handleOpenFolder(row)"
|
||
>
|
||
{{t('vabI18n.knowledge.document.buttons.open', '打开')}}
|
||
</el-button>
|
||
</template>
|
||
<!-- 共同操作 -->
|
||
<el-button
|
||
type="warning"
|
||
:icon="Edit"
|
||
text
|
||
class="action-btn"
|
||
@click="handleRename(row)"
|
||
>
|
||
{{t('vabI18n.knowledge.document.buttons.rename')}}
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
:icon="Delete"
|
||
text
|
||
class="action-btn"
|
||
@click="handleDelete(row)"
|
||
>
|
||
{{t('vabI18n.knowledge.document.buttons.delete')}}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination-wrapper">
|
||
<el-pagination
|
||
v-model:current-page="pagination.current"
|
||
v-model:page-size="pagination.size"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="pagination.total"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="handlePaginationChange"
|
||
@size-change="handlePaginationChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传文件对话框 - 使用新的DocUpload组件 -->
|
||
<el-dialog
|
||
v-model="uploadDialogVisible"
|
||
:title="t('vabI18n.knowledge.document.buttons.upload')"
|
||
width="900px"
|
||
:close-on-click-modal="false"
|
||
class="upload-dialog"
|
||
>
|
||
<DocUpload
|
||
:visible="uploadDialogVisible"
|
||
:dataset-id="datasetId"
|
||
:parent-id="currentParentId"
|
||
@update:visible="uploadDialogVisible = $event"
|
||
@success="handleUploadSuccess"
|
||
/>
|
||
</el-dialog>
|
||
|
||
<!-- 文件替换对话框 -->
|
||
<el-dialog
|
||
v-model="replaceDialogVisible"
|
||
title="替换文件"
|
||
width="900px"
|
||
:close-on-click-modal="false"
|
||
class="upload-dialog"
|
||
>
|
||
<div v-if="replaceTargetFile" class="replace-info">
|
||
<div class="target-file-info">
|
||
<h4>将要替换的文件:</h4>
|
||
<div class="file-info-card">
|
||
<img
|
||
:src="getFileTypeIcon(replaceTargetFile.fileType)"
|
||
alt="文件图标"
|
||
class="file-icon"
|
||
/>
|
||
<div class="file-details">
|
||
<div class="file-name">{{ replaceTargetFile.fileName }}</div>
|
||
<div class="file-meta">
|
||
<span>大小:{{ formatFileSize(replaceTargetFile.charCount) }}</span>
|
||
<span>创建时间:{{ replaceTargetFile.createDate }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<DocUpload
|
||
:visible="replaceDialogVisible"
|
||
:dataset-id="datasetId"
|
||
:parent-id="currentParentId"
|
||
:replace-file-id="replaceTargetFile?.id"
|
||
@update:visible="replaceDialogVisible = $event"
|
||
@success="handleReplaceSuccess"
|
||
/>
|
||
</el-dialog>
|
||
|
||
<!-- 预览抽屉 -->
|
||
|
||
<el-drawer
|
||
v-model="previewDrawerVisible"
|
||
:title="t('vabI18n.knowledge.document.preview.title')"
|
||
:direction="'rtl'"
|
||
size="80%"
|
||
class="preview-drawer"
|
||
:close-on-click-modal="false"
|
||
@close="handleClose"
|
||
>
|
||
<div v-loading="previewLoading" class="drawer-content-container" style="display: flex; height: 100%;">
|
||
|
||
<!-- 左侧分段数据容器 -->
|
||
<div class="segment-list-container" style="width: 30%; border-right: 1px solid #ebeef5; overflow-y: auto; padding: 10px;">
|
||
<h3>分段数据</h3>
|
||
<el-form v-if="segmentList.length > 0">
|
||
<el-form-item
|
||
v-for="(segment) in segmentList"
|
||
:key="segment.id"
|
||
style="cursor: pointer; margin-bottom: 5px;"
|
||
>
|
||
<span>分段{{segment.position}}:</span>
|
||
<h4>{{segment.content}}</h4>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div v-else>
|
||
暂无分段数据
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧原预览内容 -->
|
||
<div class="preview-content-container" style="flex: 1; padding: 10px; overflow-y: auto;">
|
||
<!-- TXT 文件预览 -->
|
||
<div v-if="previewFileType === 'txt'" class="text-preview-container">
|
||
<pre class="text-preview">{{ previewTextContent }}</pre>
|
||
</div>
|
||
|
||
<!-- Markdown 文件预览 -->
|
||
<div v-if="['md', 'markdown', 'mdx'].includes(previewFileType.toLowerCase())"
|
||
class="markdown-preview-container">
|
||
<div class="markdown-preview" v-html="previewMarkdownContent"></div>
|
||
</div>
|
||
|
||
<vue-office-pdf
|
||
v-if="previewFileType === 'pdf'"
|
||
:src="previewFileUrl"
|
||
@rendered="renderedHandler"
|
||
@error="errorHandler"
|
||
/>
|
||
|
||
<vue-office-docx
|
||
v-if="previewFileType == 'doc' || previewFileType == 'docx'"
|
||
:src="previewFileUrl"
|
||
@rendered="renderedHandler"
|
||
@error="errorHandler"
|
||
/>
|
||
|
||
<vue-office-excel
|
||
v-if="previewFileType == 'xlsx' || previewFileType == 'xls' || previewFileType == 'csv'"
|
||
:src="previewFileUrl"
|
||
style="height: 100vh;"
|
||
@rendered="renderedHandler"
|
||
@error="errorHandler"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</el-drawer>
|
||
|
||
|
||
<!-- 重命名对话框 -->
|
||
<el-dialog
|
||
v-model="renameDialogVisible"
|
||
:title="t('vabI18n.knowledge.document.renameDialog.title')"
|
||
width="480px"
|
||
class="rename-dialog"
|
||
:close-on-click-modal="false"
|
||
align-center
|
||
>
|
||
<div class="dialog-content">
|
||
<div class="dialog-icon">
|
||
<el-icon size="48" color="#409eff"><Edit /></el-icon>
|
||
</div>
|
||
<el-form :model="renameForm" label-width="0px" class="rename-form">
|
||
<el-form-item>
|
||
<div class="form-label">{{t('vabI18n.knowledge.document.renameDialog.label')}}</div>
|
||
<el-input
|
||
v-model="renameForm.newName"
|
||
:placeholder="t('vabI18n.knowledge.document.renameDialog.placeholder')"
|
||
size="large"
|
||
class="rename-input"
|
||
@keyup.enter="handleRenameConfirm"
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Document /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button size="large" @click="renameDialogVisible = false">
|
||
{{t('vabI18n.knowledge.document.renameDialog.cancel')}}
|
||
</el-button>
|
||
<el-button type="primary" size="large" @click="handleRenameConfirm">
|
||
<el-icon><Check /></el-icon>
|
||
{{t('vabI18n.knowledge.document.renameDialog.confirm')}}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 创建文件夹对话框 -->
|
||
<el-dialog
|
||
v-model="createFolderDialogVisible"
|
||
:title="t('vabI18n.knowledge.document.createFolderDialog.title', '创建文件夹')"
|
||
width="520px"
|
||
class="create-folder-dialog"
|
||
:close-on-click-modal="false"
|
||
align-center
|
||
>
|
||
<div class="dialog-content">
|
||
<div class="dialog-icon">
|
||
<el-icon size="48" color="#67c23a"><FolderAdd /></el-icon>
|
||
</div>
|
||
<el-form :model="createFolderForm" :rules="createFolderRules" ref="createFolderFormRef" label-width="0px" class="create-folder-form">
|
||
<el-form-item prop="folderName">
|
||
<div class="form-label">{{t('vabI18n.knowledge.document.createFolderDialog.label', '文件夹名称')}}</div>
|
||
<el-input
|
||
v-model="createFolderForm.folderName"
|
||
:placeholder="t('vabI18n.knowledge.document.createFolderDialog.placeholder', '请输入文件夹名称')"
|
||
size="large"
|
||
class="folder-name-input"
|
||
@keyup.enter="handleCreateFolderConfirm"
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Folder /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button size="large" @click="createFolderDialogVisible = false">
|
||
{{t('vabI18n.knowledge.document.createFolderDialog.cancel', '取消')}}
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
size="large"
|
||
@click="handleCreateFolderConfirm"
|
||
:loading="createFolderLoading"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
{{t('vabI18n.knowledge.document.createFolderDialog.confirm', '创建')}}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 右键菜单 -->
|
||
<transition name="context-menu-fade">
|
||
<div
|
||
v-if="contextMenuVisible"
|
||
class="context-menu"
|
||
:style="{ left: contextMenuPosition.x + 'px', top: contextMenuPosition.y + 'px' }"
|
||
@click.stop
|
||
>
|
||
<div class="context-menu-header">
|
||
<div class="menu-title">操作菜单</div>
|
||
</div>
|
||
<div class="context-menu-item" @click="handleRefresh">
|
||
<div class="item-icon">
|
||
<el-icon><Refresh /></el-icon>
|
||
</div>
|
||
<div class="item-content">
|
||
<div class="item-title">{{t('vabI18n.knowledge.document.contextMenu.refresh', '刷新')}}</div>
|
||
<div class="item-desc">刷新文件列表</div>
|
||
</div>
|
||
</div>
|
||
<div class="context-menu-item" @click="handleCreateFolderFromContext">
|
||
<div class="item-icon">
|
||
<el-icon><FolderAdd /></el-icon>
|
||
</div>
|
||
<div class="item-content">
|
||
<div class="item-title">{{t('vabI18n.knowledge.document.contextMenu.createFolder', '新建文件夹')}}</div>
|
||
<div class="item-desc">在当前目录创建文件夹</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||
import VueOfficePdf from '@vue-office/pdf'
|
||
import { getDatasetDocPage, uploadDocument, deleteDocument, downloadDocument, previewDocumentUrl, renameDocument, createFolder, CreateFolderReq} from '@/api/dataset'
|
||
import {getSegmentList} from "@/api/Segment"
|
||
import DocUpload from './DocUpload.vue'
|
||
//引入VueOfficeDocx组件
|
||
import VueOfficeDocx from '@vue-office/docx'
|
||
//引入相关样式
|
||
import '@vue-office/docx/lib/index.css'
|
||
//引入VueOfficeExcel组件
|
||
import VueOfficeExcel from '@vue-office/excel'
|
||
//引入相关样式
|
||
import '@vue-office/excel/lib/index.css'
|
||
|
||
|
||
import VueOfficePptx from '@vue-office/pptx'
|
||
|
||
// 引入 markdown 解析器和代码高亮
|
||
import { marked } from 'marked'
|
||
import hljs from 'highlight.js'
|
||
import 'highlight.js/styles/github.css'
|
||
|
||
import { ElLoading, ElMessage, ElNotification, ElMessageBox } from 'element-plus'
|
||
|
||
|
||
import {
|
||
Search,
|
||
Refresh,
|
||
Upload,
|
||
Download,
|
||
View,
|
||
Edit,
|
||
Delete,
|
||
Check,
|
||
Close,
|
||
Loading,
|
||
Document,
|
||
InfoFilled,
|
||
ArrowLeft,
|
||
Folder,
|
||
FolderAdd,
|
||
House,
|
||
Plus
|
||
} from '@element-plus/icons-vue'
|
||
import { watch } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
const { t, locale } = useI18n()
|
||
|
||
|
||
|
||
const userStore = useUserStore()
|
||
const aclStore = useAclStore()
|
||
const { token } = userStore
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const datasetId = ref('');
|
||
const datasetName = ref('');
|
||
const previewLoading = ref(true)
|
||
|
||
const uploadDialogVisible = ref(false)
|
||
const replaceDialogVisible = ref(false)
|
||
const replaceTargetFile = ref<FileItem | null>(null)
|
||
|
||
// 重命名相关
|
||
const renameDialogVisible = ref(false)
|
||
const renameForm = reactive({
|
||
id: '',
|
||
newName: ''
|
||
})
|
||
|
||
// 创建文件夹相关
|
||
const createFolderDialogVisible = ref(false)
|
||
const createFolderLoading = ref(false)
|
||
const createFolderFormRef = ref()
|
||
const createFolderForm = reactive({
|
||
folderName: ''
|
||
})
|
||
|
||
// 表单验证规则
|
||
const createFolderRules = {
|
||
folderName: [
|
||
{ required: true, message: t('vabI18n.knowledge.document.createFolderDialog.rules.nameRequired', '文件夹名称不能为空'), trigger: 'blur' },
|
||
{ max: 50, message: t('vabI18n.knowledge.document.createFolderDialog.rules.nameMaxLength', '文件夹名称不能超过50个字符'), trigger: 'blur' },
|
||
{ pattern: /^[^<>:"/\\|?*]+$/, message: t('vabI18n.knowledge.document.createFolderDialog.rules.namePattern', '文件夹名称不能包含特殊字符'), trigger: 'blur' }
|
||
]
|
||
}
|
||
const segmentList= ref([])
|
||
// 右键菜单相关
|
||
const contextMenuVisible = ref(false)
|
||
const contextMenuPosition = reactive({
|
||
x: 0,
|
||
y: 0
|
||
})
|
||
|
||
// 面包屑导航相关
|
||
const currentParentId = ref<number | undefined>(undefined)
|
||
const breadcrumbPath = ref<Array<{id: number, name: string}>>([])
|
||
|
||
// 面包屑导航项接口
|
||
interface BreadcrumbItem {
|
||
id: number;
|
||
name: string;
|
||
}
|
||
|
||
|
||
|
||
const handleClose = () => {
|
||
uploadDialogVisible.value = false
|
||
segmentList.value = []
|
||
}
|
||
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
// 清理右键菜单事件监听器
|
||
document.removeEventListener('click', hideContextMenu)
|
||
// 停止自动刷新定时器
|
||
stopAutoRefresh()
|
||
})
|
||
|
||
const getFileTypeIcon = (fileType: string) => {
|
||
const icons = require.context('@/assets/img/filetype-icon', false, /\.png$/)
|
||
// console.log(icons)
|
||
// console.log('所有图标 keys:', icons.keys())
|
||
|
||
const getIconUrl = (iconName: string) => {
|
||
const fullName = `./${iconName}.png`
|
||
if (icons.keys().includes(fullName)) {
|
||
return icons(fullName)
|
||
} else {
|
||
// console.warn('图标不存在:', fullName)
|
||
return ''
|
||
}
|
||
}
|
||
|
||
const iconMap: Record<string, string> = {
|
||
'pdf': getIconUrl('pdf'),
|
||
'doc': getIconUrl('word'),
|
||
'docx': getIconUrl('word'),
|
||
'xls': getIconUrl('excel'),
|
||
'xlsx': getIconUrl('excel'),
|
||
'ppt': getIconUrl('ppt'),
|
||
'pptx': getIconUrl('ppt'),
|
||
'txt': getIconUrl('txt'),
|
||
'md': getIconUrl('txt'),
|
||
'markdown': getIconUrl('txt'),
|
||
'mdx': getIconUrl('txt'),
|
||
'html': getIconUrl('txt'),
|
||
'htm': getIconUrl('txt'),
|
||
'csv': getIconUrl('excel'),
|
||
'vtt': getIconUrl('txt'),
|
||
'properties': getIconUrl('txt'),
|
||
'xml': getIconUrl('xml'),
|
||
'zip': getIconUrl('zip'),
|
||
'mp4': getIconUrl('video'),
|
||
'avi': getIconUrl('video'),
|
||
'mp3': getIconUrl('audio'),
|
||
'wav': getIconUrl('audio'),
|
||
'jpg': getIconUrl('img'),
|
||
'jpeg': getIconUrl('img'),
|
||
'png': getIconUrl('img'),
|
||
'gif': getIconUrl('img'),
|
||
}
|
||
return iconMap[fileType.toLowerCase()] || getIconUrl('unknown')
|
||
}
|
||
|
||
// 多选相关
|
||
const selectedRows = ref<FileItem[]>([])
|
||
|
||
interface FileItem {
|
||
id: string
|
||
fileName: string
|
||
fileType: string
|
||
parseStatus: string
|
||
previewConvert?: string
|
||
size?: string
|
||
charCount: number
|
||
createDate: string
|
||
status: string
|
||
// 新增字段,适配TDatasetFiles
|
||
type: string // file-文件, folder-文件夹
|
||
parentId?: number
|
||
path: string
|
||
mimeType?: string
|
||
difyDocId?: string
|
||
difyStoragePath?: string
|
||
sourceUrl?: string // 新增:文档源URL,用于下载和预览
|
||
}
|
||
|
||
// 搜索关键词
|
||
const searchKeyword = ref('')
|
||
|
||
// 分页数据
|
||
const pagination = reactive({
|
||
current: 1,
|
||
size: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 排序数据
|
||
const sortConfig = reactive({
|
||
orderBy: 'name',
|
||
orderDirection: 'ASC'
|
||
})
|
||
|
||
// 文件列表数据
|
||
const fileList = ref<FileItem[]>([])
|
||
|
||
|
||
//预览右抽屉状态
|
||
const previewDrawerVisible = ref(false)
|
||
const previewFileType = ref('')
|
||
const previewFileUrl = ref('')
|
||
// 添加文本内容预览状态
|
||
const previewTextContent = ref('')
|
||
const previewMarkdownContent = ref('')
|
||
|
||
// 选择变化处理
|
||
const handleSelectionChange = (selection: FileItem[]) => {
|
||
selectedRows.value = selection
|
||
}
|
||
|
||
// 批量删除处理
|
||
const handleBatchDelete = async () => {
|
||
if (selectedRows.value.length === 0) {
|
||
ElMessage.warning(t('vabI18n.knowledge.document.messages.noFileSelected'))
|
||
return
|
||
}
|
||
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
t('vabI18n.knowledge.document.messages.batchDeleteConfirm', {
|
||
count: selectedRows.value.length
|
||
}),
|
||
t('vabI18n.knowledge.document.messages.deleteConfirmTitle'),
|
||
{
|
||
confirmButtonText: t('vabI18n.knowledge.document.buttons.confirm'),
|
||
cancelButtonText: t('vabI18n.knowledge.document.buttons.cancel'),
|
||
type: 'warning'
|
||
}
|
||
)
|
||
|
||
const handleClose = () => {
|
||
previewDrawerVisible.value = false,
|
||
segmentList.value = []
|
||
}
|
||
const loading = ElLoading.service({
|
||
lock: true,
|
||
text: t('vabI18n.knowledge.document.messages.deleteing'),
|
||
background: 'rgba(0, 0, 0, 0.7)'
|
||
})
|
||
|
||
try {
|
||
// 批量删除
|
||
for (const row of selectedRows.value) {
|
||
await deleteDocument(parseInt(row.id))
|
||
}
|
||
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.messages.deleteSuccessOk'),
|
||
message: t('vabI18n.knowledge.document.messages.batchDeleteSuccess') + ` ${selectedRows.value.length} ` + t('vabI18n.knowledge.document.messages.batchDeleteSuccessEnd'),
|
||
type: 'success'
|
||
})
|
||
|
||
// 清空选择
|
||
selectedRows.value = []
|
||
// 刷新文件列表
|
||
await fetchDocuments()
|
||
} finally {
|
||
loading.close()
|
||
}
|
||
} catch (error) {
|
||
console.error('批量删除失败:', error)
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.deleteFailed'),
|
||
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.deleteFailed'),
|
||
type: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
const originalExtension = ref('')
|
||
// 重命名处理
|
||
const handleRename = (row: FileItem) => {
|
||
renameForm.id = row.id
|
||
const parts = row.fileName.split('.')
|
||
if (parts.length > 1) {
|
||
originalExtension.value = parts.pop()!
|
||
renameForm.newName = parts.join('.')
|
||
} else {
|
||
originalExtension.value = ''
|
||
renameForm.newName = row.fileName
|
||
}
|
||
|
||
renameDialogVisible.value = true
|
||
}
|
||
|
||
// 重命名确认
|
||
const handleRenameConfirm = async () => {
|
||
|
||
const newName = (originalExtension.value
|
||
? renameForm.newName.trim() + '.' + originalExtension.value
|
||
: renameForm.newName.trim())
|
||
|
||
if (!newName || newName === '.') {
|
||
ElMessage.warning(t('vabI18n.knowledge.document.messages.FilenamecantEmpty'))
|
||
return
|
||
}
|
||
|
||
try {
|
||
await renameDocument({
|
||
fileId: parseInt(renameForm.id),
|
||
newName: newName
|
||
})
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.renameSuccess'),
|
||
message: t('vabI18n.knowledge.document.messages.renameSuccess'),
|
||
type: 'success'
|
||
})
|
||
|
||
renameDialogVisible.value = false
|
||
await fetchDocuments()
|
||
} catch (error) {
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.renameFailed'),
|
||
message: t('vabI18n.knowledge.document.messages.renameFailed'),
|
||
type: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
|
||
// 数字格式化
|
||
const formatNumber = (num: number) => {
|
||
return num.toLocaleString()
|
||
}
|
||
|
||
// 文件大小格式化
|
||
const formatFileSize = (bytes: number): string => {
|
||
if (bytes === 0) return '0 B'
|
||
const k = 1024
|
||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||
}
|
||
|
||
// 处理取消上传
|
||
// 监听预览抽屉关闭事件,清理预览内容
|
||
watch(previewDrawerVisible, (newVal) => {
|
||
if (!newVal) {
|
||
previewTextContent.value = ''
|
||
previewMarkdownContent.value = ''
|
||
previewFileUrl.value = ''
|
||
previewFileType.value = ''
|
||
}
|
||
})
|
||
|
||
import { useUserStore } from '@/store/modules/user'
|
||
import { useAclStore } from '@/store/modules/acl'
|
||
import vab from '~/library/plugins/vab'
|
||
|
||
// 事件处理函数
|
||
const renderedHandler = () => {
|
||
previewLoading.value = false
|
||
console.log("渲染完成")
|
||
}
|
||
|
||
const errorHandler = () => {
|
||
previewLoading.value = false
|
||
console.log("渲染失败")
|
||
}
|
||
|
||
const triggerFileInput = () => {
|
||
uploadDialogVisible.value = true
|
||
}
|
||
|
||
// 文件替换处理 - 从行操作触发
|
||
const handleFileReplaceFromRow = (row: FileItem) => {
|
||
replaceTargetFile.value = row
|
||
replaceDialogVisible.value = true
|
||
}
|
||
|
||
const indexMethod = (index: number) => {
|
||
return (pagination.current - 1) * pagination.size + index + 1
|
||
}
|
||
|
||
|
||
|
||
// 处理DocUpload组件的上传成功回调
|
||
const handleUploadSuccess = async () => {
|
||
uploadDialogVisible.value = false
|
||
await fetchDocuments()
|
||
// ElNotification({
|
||
// title: t('vabI18n.knowledge.document.messages.uploadSuccess'),
|
||
// message: t('vabI18n.knowledge.document.messages.uploadSuccessEnd'),
|
||
// type: 'success'
|
||
// })
|
||
}
|
||
|
||
// 处理文件替换成功回调
|
||
const handleReplaceSuccess = async () => {
|
||
replaceDialogVisible.value = false
|
||
replaceTargetFile.value = null
|
||
selectedRows.value = [] // 清空选择
|
||
await fetchDocuments()
|
||
ElNotification({
|
||
title: '替换成功',
|
||
message: '文件替换成功',
|
||
type: 'success'
|
||
})
|
||
}
|
||
|
||
//文件下载
|
||
const handleDownload = async (row: FileItem) => {
|
||
try {
|
||
// 使用新的API下载方式,传入fileId(数字类型)
|
||
const response = await downloadDocument(parseInt(row.id))
|
||
|
||
// 创建 Blob 对象
|
||
const blob = new Blob([response.data], { type: 'application/octet-stream' })
|
||
|
||
// 创建下载链接
|
||
const url = window.URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = row.fileName // 使用文件原始名称
|
||
link.style.display = 'none'
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
|
||
// 清理
|
||
document.body.removeChild(link)
|
||
window.URL.revokeObjectURL(url)
|
||
|
||
} catch (error) {
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.downloadFailed'),
|
||
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.NoKnowError'),
|
||
type: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
//获取预览路径
|
||
const fetchPreviewUrl = async (fileId: number) => {
|
||
const response = await previewDocumentUrl(fileId)
|
||
console.log("response",response)
|
||
return response.data
|
||
}
|
||
|
||
// 获取文件文本内容(用于txt和markdown预览)
|
||
const fetchFileContent = async (fileId: number, fileType: string) => {
|
||
try {
|
||
// 对于txt和markdown文件,我们需要获取原始文本内容
|
||
// 这里可以通过下载接口获取文件内容,然后转为文本
|
||
const response = await downloadDocument(fileId)
|
||
|
||
// 将响应数据转为文本
|
||
if (response.data instanceof Blob) {
|
||
const text = await response.data.text()
|
||
return text
|
||
} else if (typeof response.data === 'string') {
|
||
return response.data
|
||
} else {
|
||
return JSON.stringify(response.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('获取文件内容失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
const handlePreview = async (row: FileItem) => {
|
||
// 显示抽屉
|
||
previewDrawerVisible.value = true
|
||
previewLoading.value = true
|
||
|
||
previewFileType.value = row.fileType;
|
||
|
||
try {
|
||
// 对于txt和markdown文件,获取文本内容
|
||
if (['txt', 'md', 'markdown', 'mdx'].includes(row.fileType.toLowerCase())) {
|
||
const content = await fetchFileContent(parseInt(row.id), row.fileType)
|
||
|
||
if (row.fileType.toLowerCase() === 'txt') {
|
||
previewTextContent.value = content
|
||
} else if (['md', 'markdown', 'mdx'].includes(row.fileType.toLowerCase())) {
|
||
// 配置并解析 markdown
|
||
const renderer = new marked.Renderer()
|
||
|
||
marked.setOptions({
|
||
breaks: true,
|
||
gfm: true
|
||
})
|
||
|
||
|
||
previewMarkdownContent.value = marked.parse(content) as string
|
||
}
|
||
} else {
|
||
// 优先使用sourceUrl进行预览
|
||
if (row.sourceUrl) {
|
||
previewFileUrl.value = row.sourceUrl
|
||
console.log("previewFileUrl (from sourceUrl)", previewFileUrl.value)
|
||
} else {
|
||
// 兜底:使用新的API获取预览URL
|
||
previewFileUrl.value = await fetchPreviewUrl(parseInt(row.id))
|
||
console.log("previewFileUrl (from API)", previewFileUrl.value)
|
||
}
|
||
}
|
||
const resp = await getSegmentList({
|
||
datasetId:datasetId.value,
|
||
documentId: row.difyDocId
|
||
})
|
||
if (resp.data?.length) {
|
||
segmentList.value = resp.data
|
||
}
|
||
} catch (error) {
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.previewFailed'),
|
||
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.getFileContentFailed'),
|
||
type: 'error'
|
||
})
|
||
} finally {
|
||
previewLoading.value = false
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 在script setup部分添加loading状态
|
||
const tableLoading = ref(false)
|
||
|
||
// 定时器相关
|
||
const refreshTimer = ref<NodeJS.Timeout | null>(null)
|
||
|
||
// 启动定时器
|
||
const startAutoRefresh = () => {
|
||
stopAutoRefresh() // 先清除可能存在的定时器
|
||
refreshTimer.value = setInterval(() => {
|
||
// 只有在非加载状态下才自动刷新
|
||
if (!tableLoading.value) {
|
||
fetchDocuments()
|
||
}
|
||
}, 30000) // 5秒刷新一次
|
||
}
|
||
|
||
// 停止定时器
|
||
const stopAutoRefresh = () => {
|
||
if (refreshTimer.value) {
|
||
clearInterval(refreshTimer.value)
|
||
refreshTimer.value = null
|
||
}
|
||
}
|
||
|
||
// 修改fetchDocuments函数
|
||
const fetchDocuments = async () => {
|
||
tableLoading.value = true
|
||
try {
|
||
const params = {
|
||
difyDatasetId: datasetId.value,
|
||
parentId: currentParentId.value, // 使用当前文件夹ID
|
||
fileName: searchKeyword.value || undefined,
|
||
orderBy: sortConfig.orderBy,
|
||
orderDirection: sortConfig.orderDirection,
|
||
pageNo: pagination.current,
|
||
pageSize: pagination.size
|
||
}
|
||
|
||
const { data } = await getDatasetDocPage(params)
|
||
|
||
// 映射API数据到表格结构,适配新的TDatasetFiles字段
|
||
fileList.value = data.content.map((item: any) => ({
|
||
id: item.id,
|
||
fileName: item.name,
|
||
parseStatus: formatIndexStatus(item.indexingStatus),
|
||
fileType: item.extension?.replace('.', '').toLowerCase() || item.name.split('.').pop()?.toLowerCase() || '',
|
||
charCount: item.size || 0, // 使用文件大小,如果有字符数可以后续添加
|
||
createDate: formatTimestamp(Math.floor(new Date(item.createdAt).getTime() / 1000)),
|
||
status: item.indexingStatus,
|
||
// 新增字段
|
||
type: item.type,
|
||
parentId: item.parentId,
|
||
path: item.path,
|
||
mimeType: item.mimeType,
|
||
difyDocId: item.difyDocId,
|
||
difyStoragePath: item.difyStoragePath,
|
||
sourceUrl: item.sourceUrl // 新增:映射sourceUrl字段
|
||
}))
|
||
|
||
pagination.total = data.total
|
||
pagination.current = data.pageNo
|
||
|
||
} catch (error) {
|
||
console.error('获取文档列表失败:', error)
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.fetchFailed'),
|
||
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.NoKnowError'),
|
||
type: 'error'
|
||
})
|
||
} finally {
|
||
tableLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 添加刷新方法
|
||
const handleRefresh = () => {
|
||
searchKeyword.value = ''
|
||
pagination.current = 1
|
||
selectedRows.value = []
|
||
fetchDocuments()
|
||
}
|
||
|
||
// 返回知识库列表
|
||
const handleGoBack = () => {
|
||
router.push('/datasets')
|
||
}
|
||
|
||
const formatTimestampToLocaleString = (timestamp: number): string => {
|
||
const date = new Date(timestamp * 1000)
|
||
return date.toLocaleString()
|
||
}
|
||
|
||
// 工具函数
|
||
const formatTimestamp = (timestamp: number): string => {
|
||
const date = new Date(timestamp * 1000)
|
||
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN')
|
||
}
|
||
|
||
const formatIndexStatus = (status: string): string => {
|
||
const statusMap: Record<string, string> = {
|
||
'preprocessing': '预处理',
|
||
'indexing': '解析中',
|
||
'waiting': '等待中',
|
||
'queued': '排队中',
|
||
'failed': '解析失败',
|
||
'completed': '解析成功'
|
||
}
|
||
return statusMap[status] || '未知状态'
|
||
}
|
||
|
||
// 获取状态对应的标签类型
|
||
const getStatusTagType = (status: string): 'success' | 'primary' | 'warning' | 'info' | 'danger' => {
|
||
const typeMap: Record<string, 'success' | 'primary' | 'warning' | 'info' | 'danger'> = {
|
||
'preprocessing': 'info',
|
||
'indexing': 'warning',
|
||
'waiting': 'info',
|
||
'queued': 'info', // 修改为info类型,显示较深灰色
|
||
'failed': 'danger',
|
||
'completed': 'success'
|
||
}
|
||
return typeMap[status] || 'info'
|
||
}
|
||
|
||
// 获取状态对应的图标
|
||
const getStatusIcon = (status: string) => {
|
||
const iconMap: Record<string, any> = {
|
||
'preprocessing': Loading,
|
||
'indexing': Loading,
|
||
'waiting': Loading,
|
||
'queued': Loading,
|
||
'failed': Close,
|
||
'completed': Check
|
||
}
|
||
return iconMap[status] || InfoFilled
|
||
}
|
||
|
||
const handleDelete = async (row: FileItem) => {
|
||
try {
|
||
// 确认对话框
|
||
await ElMessageBox.confirm(t('vabI18n.knowledge.document.messages.deleteConfirm'), t('vabI18n.knowledge.deleteConfirm.title'), {
|
||
confirmButtonText: t('vabI18n.knowledge.deleteConfirm.confirm'),
|
||
cancelButtonText: t('vabI18n.knowledge.deleteConfirm.cancel'),
|
||
type: 'warning'
|
||
})
|
||
|
||
// 调用删除接口
|
||
await deleteDocument(parseInt(row.id))
|
||
|
||
// 成功处理
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.messages.deleteSuccessOk'),
|
||
message: `${t('vabI18n.knowledge.document.messages.deleteSuccess')}"${row.fileName}"${t('vabI18n.knowledge.document.messages.deleteSuccessEnd')}`,
|
||
type: 'success'
|
||
})
|
||
|
||
// 刷新文件列表
|
||
await fetchDocuments()
|
||
} catch (error) {
|
||
console.error('删除文件失败:', error)
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.deleteFailed'),
|
||
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.NoKnowError'),
|
||
type: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
datasetId.value = route.params.id as string
|
||
datasetName.value = route.query.name as string
|
||
// 这里可以调用API获取详细数据
|
||
await fetchDocuments()
|
||
// 启动自动刷新定时器
|
||
startAutoRefresh()
|
||
})
|
||
|
||
// 分页变化处理
|
||
const handlePaginationChange = () => {
|
||
fetchDocuments()
|
||
}
|
||
|
||
// 搜索处理
|
||
const handleSearch = () => {
|
||
pagination.current = 1
|
||
fetchDocuments()
|
||
}
|
||
|
||
// 排序变化处理
|
||
const handleSortChange = (sortInfo: any) => {
|
||
if (sortInfo.prop && sortInfo.order) {
|
||
// 映射前端字段到后端字段
|
||
const fieldMapping: Record<string, string> = {
|
||
'fileName': 'name',
|
||
'createDate': 'created_at',
|
||
'charCount': 'size'
|
||
}
|
||
|
||
sortConfig.orderBy = fieldMapping[sortInfo.prop] || sortInfo.prop
|
||
sortConfig.orderDirection = sortInfo.order === 'ascending' ? 'ASC' : 'DESC'
|
||
|
||
// 重置到第一页并刷新数据
|
||
pagination.current = 1
|
||
fetchDocuments()
|
||
}
|
||
}
|
||
|
||
// 处理文件夹打开
|
||
const handleOpenFolder = (row: FileItem) => {
|
||
// 设置当前文件夹ID并更新面包屑路径
|
||
currentParentId.value = parseInt(row.id)
|
||
breadcrumbPath.value.push({
|
||
id: parseInt(row.id),
|
||
name: row.fileName
|
||
})
|
||
|
||
// 重置分页到第一页
|
||
pagination.current = 1
|
||
// 刷新文件列表
|
||
fetchDocuments()
|
||
}
|
||
|
||
// 表格行双击处理
|
||
const handleRowDoubleClick = (row: FileItem) => {
|
||
if (row.type === 'folder') {
|
||
handleOpenFolder(row)
|
||
} else if (row.type === 'file' && row.status === 'completed') {
|
||
// 如果是文件且状态为已完成,则预览
|
||
handlePreview(row)
|
||
}
|
||
}
|
||
|
||
// 面包屑导航处理
|
||
const navigateToRoot = () => {
|
||
currentParentId.value = undefined
|
||
breadcrumbPath.value = []
|
||
pagination.current = 1
|
||
fetchDocuments()
|
||
}
|
||
|
||
const navigateToFolder = (folder: BreadcrumbItem, index: number) => {
|
||
// 如果点击的不是最后一个面包屑项,则导航到该文件夹
|
||
if (index < breadcrumbPath.value.length - 1) {
|
||
currentParentId.value = folder.id
|
||
breadcrumbPath.value = breadcrumbPath.value.slice(0, index + 1)
|
||
pagination.current = 1
|
||
fetchDocuments()
|
||
}
|
||
}
|
||
|
||
// 右键菜单处理
|
||
const handleRightClick = (event: MouseEvent) => {
|
||
// 只在空白区域右键时显示菜单
|
||
const target = event.target as HTMLElement
|
||
if (target.closest('.el-table') || target.closest('.action-bar')) {
|
||
return
|
||
}
|
||
|
||
contextMenuPosition.x = event.clientX
|
||
contextMenuPosition.y = event.clientY
|
||
contextMenuVisible.value = true
|
||
|
||
// 监听点击事件来隐藏菜单
|
||
document.addEventListener('click', hideContextMenu)
|
||
}
|
||
|
||
const hideContextMenu = () => {
|
||
contextMenuVisible.value = false
|
||
document.removeEventListener('click', hideContextMenu)
|
||
}
|
||
|
||
// 从右键菜单创建文件夹
|
||
const handleCreateFolderFromContext = () => {
|
||
hideContextMenu()
|
||
createFolderForm.folderName = ''
|
||
createFolderDialogVisible.value = true
|
||
}
|
||
|
||
// 创建文件夹确认
|
||
const handleCreateFolderConfirm = async () => {
|
||
if (!createFolderFormRef.value) return
|
||
|
||
try {
|
||
await createFolderFormRef.value.validate()
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
createFolderLoading.value = true
|
||
|
||
try {
|
||
const folderData: CreateFolderReq = {
|
||
datasetId: datasetId.value,
|
||
folderName: createFolderForm.folderName,
|
||
parentId: currentParentId.value, // 在当前目录下创建文件夹
|
||
ownerId: aclStore.getUserId || 1 // 从acl store获取用户ID
|
||
}
|
||
|
||
await createFolder(folderData)
|
||
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.messages.createFolderSuccess', '创建成功'),
|
||
message: t('vabI18n.knowledge.document.messages.createFolderSuccessMessage', `文件夹"${createFolderForm.folderName}"创建成功`),
|
||
type: 'success'
|
||
})
|
||
|
||
createFolderDialogVisible.value = false
|
||
createFolderForm.folderName = ''
|
||
|
||
// 刷新文件列表
|
||
await fetchDocuments()
|
||
} catch (error) {
|
||
console.error('创建文件夹失败:', error)
|
||
ElNotification({
|
||
title: t('vabI18n.knowledge.document.errors.createFolderFailed', '创建失败'),
|
||
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.createFolderFailed', '创建文件夹失败'),
|
||
type: 'error'
|
||
})
|
||
} finally {
|
||
createFolderLoading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.main-container {
|
||
padding: 20px;
|
||
background: #f5f7fa;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
|
||
.header-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
|
||
.back-button {
|
||
color: #409eff;
|
||
font-size: 14px;
|
||
padding: 8px 12px;
|
||
|
||
&:hover {
|
||
background-color: #ecf5ff;
|
||
color: #337ecc;
|
||
}
|
||
}
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin: 0;
|
||
}
|
||
|
||
.page-description {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
margin: 0;
|
||
}
|
||
|
||
.breadcrumb-container {
|
||
margin: 12px 0;
|
||
|
||
.breadcrumb-clickable {
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
::v-deep .el-breadcrumb__inner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
|
||
&.is-link:hover {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
::v-deep .el-breadcrumb__item:last-child .el-breadcrumb__inner {
|
||
color: #303133;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 内容布局样式
|
||
.content-layout {
|
||
display: flex;
|
||
gap: 20px;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
|
||
|
||
.file-manager-container {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 24px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.action-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.left-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.search-input {
|
||
width: 280px;
|
||
}
|
||
|
||
.right-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.batch-delete-btn {
|
||
animation: fadeIn 0.3s ease-in-out;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateX(-10px); }
|
||
to { opacity: 1; transform: translateX(0); }
|
||
}
|
||
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { transform: scale(1); }
|
||
50% { transform: scale(1.05); }
|
||
100% { transform: scale(1); }
|
||
}
|
||
|
||
@keyframes bounce {
|
||
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
|
||
40% { transform: translateY(-10px); }
|
||
60% { transform: translateY(-5px); }
|
||
}
|
||
|
||
.file-table {
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
|
||
|
||
::v-deep .el-table__header {
|
||
background: #fafafa;
|
||
}
|
||
|
||
::v-deep .el-table__header th {
|
||
background: #fafafa;
|
||
color: #606266;
|
||
font-weight: 600;
|
||
border-bottom: 2px solid #e4e7ed;
|
||
height: 50px;
|
||
}
|
||
|
||
::v-deep .el-table__body tr {
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
background: #f5f7fa !important;
|
||
}
|
||
}
|
||
|
||
::v-deep .el-table__body td {
|
||
height: 50px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
}
|
||
|
||
.file-name-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
|
||
.file-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
flex-shrink: 0;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.folder-icon {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.file-name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.char-count {
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: nowrap;
|
||
|
||
.action-btn {
|
||
font-size: 12px;
|
||
padding: 4px 6px;
|
||
border-radius: 4px;
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
|
||
&:hover:not(.disabled-btn) {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
&.disabled-btn {
|
||
color: #c0c4cc !important;
|
||
cursor: not-allowed !important;
|
||
background: transparent !important;
|
||
border-color: transparent !important;
|
||
|
||
&:hover {
|
||
transform: none !important;
|
||
box-shadow: none !important;
|
||
color: #c0c4cc !important;
|
||
background: transparent !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
margin-top: 24px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
|
||
// 上传对话框样式
|
||
.upload-dialog {
|
||
::v-deep .el-dialog {
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
::v-deep .el-dialog__header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 0;
|
||
padding: 24px 32px;
|
||
}
|
||
|
||
::v-deep .el-dialog__title {
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 20px;
|
||
}
|
||
|
||
::v-deep .el-dialog__close {
|
||
color: white;
|
||
font-size: 20px;
|
||
|
||
&:hover {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
|
||
::v-deep .el-dialog__body {
|
||
padding: 32px;
|
||
background: #fafbfc;
|
||
}
|
||
|
||
::v-deep .el-dialog__footer {
|
||
padding: 20px 32px 32px;
|
||
background: #fafbfc;
|
||
border-top: 1px solid #e9ecef;
|
||
}
|
||
}
|
||
|
||
// 重命名对话框样式
|
||
.rename-dialog {
|
||
::v-deep .el-dialog {
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
box-shadow: 0 25px 80px rgba(64, 158, 255, 0.15);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
::v-deep .el-dialog__header {
|
||
background: linear-gradient(135deg, #409eff 0%, #36cfc9 100%);
|
||
color: white;
|
||
border-radius: 0;
|
||
padding: 24px 32px;
|
||
text-align: center;
|
||
}
|
||
|
||
::v-deep .el-dialog__title {
|
||
color: white;
|
||
font-weight: 700;
|
||
font-size: 20px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
::v-deep .el-dialog__close {
|
||
color: white;
|
||
font-size: 20px;
|
||
|
||
&:hover {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
transform: scale(1.1) rotate(90deg);
|
||
transition: all 0.3s ease;
|
||
}
|
||
}
|
||
|
||
::v-deep .el-dialog__body {
|
||
padding: 40px 32px 32px;
|
||
background: linear-gradient(135deg, #f8fbff 0%, #f0f9ff 100%);
|
||
}
|
||
|
||
.dialog-content {
|
||
text-align: center;
|
||
|
||
.dialog-icon {
|
||
margin-bottom: 24px;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.rename-form {
|
||
.form-label {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 16px;
|
||
text-align: left;
|
||
}
|
||
|
||
.rename-input {
|
||
::v-deep .el-input__wrapper {
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
|
||
border: 2px solid transparent;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.15);
|
||
}
|
||
|
||
&.is-focus {
|
||
border-color: #409eff;
|
||
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
padding: 20px 0;
|
||
background: linear-gradient(135deg, #f8fbff 0%, #f0f9ff 100%);
|
||
|
||
.el-button {
|
||
border-radius: 12px;
|
||
padding: 12px 24px;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
&.el-button--primary {
|
||
background: linear-gradient(135deg, #409eff 0%, #36cfc9 100%);
|
||
border: none;
|
||
|
||
&:hover {
|
||
background: linear-gradient(135deg, #337ecc 0%, #2bb0a8 100%);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建文件夹对话框样式
|
||
.create-folder-dialog {
|
||
::v-deep .el-dialog {
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
box-shadow: 0 25px 80px rgba(103, 194, 58, 0.15);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
::v-deep .el-dialog__header {
|
||
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
||
color: white;
|
||
border-radius: 0;
|
||
padding: 24px 32px;
|
||
text-align: center;
|
||
}
|
||
|
||
::v-deep .el-dialog__title {
|
||
color: white;
|
||
font-weight: 700;
|
||
font-size: 20px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
::v-deep .el-dialog__close {
|
||
color: white;
|
||
font-size: 20px;
|
||
|
||
&:hover {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
transform: scale(1.1) rotate(90deg);
|
||
transition: all 0.3s ease;
|
||
}
|
||
}
|
||
|
||
::v-deep .el-dialog__body {
|
||
padding: 40px 32px 32px;
|
||
background: linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%);
|
||
}
|
||
|
||
.dialog-content {
|
||
text-align: center;
|
||
|
||
.dialog-icon {
|
||
margin-bottom: 24px;
|
||
animation: bounce 2s infinite;
|
||
}
|
||
|
||
.create-folder-form {
|
||
.form-label {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 16px;
|
||
text-align: left;
|
||
}
|
||
|
||
.folder-name-input {
|
||
::v-deep .el-input__wrapper {
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 12px rgba(103, 194, 58, 0.1);
|
||
border: 2px solid transparent;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
box-shadow: 0 6px 16px rgba(103, 194, 58, 0.15);
|
||
}
|
||
|
||
&.is-focus {
|
||
border-color: #67c23a;
|
||
box-shadow: 0 8px 20px rgba(103, 194, 58, 0.2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
padding: 20px 0;
|
||
background: linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%);
|
||
|
||
.el-button {
|
||
border-radius: 12px;
|
||
padding: 12px 24px;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
&.el-button--primary {
|
||
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
||
border: none;
|
||
|
||
&:hover {
|
||
background: linear-gradient(135deg, #529e2b 0%, #6cb33f 100%);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
// 文件替换信息样式
|
||
.replace-info {
|
||
margin-bottom: 24px;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
border-left: 4px solid #e6a23c;
|
||
|
||
.target-file-info {
|
||
h4 {
|
||
margin: 0 0 16px 0;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.file-info-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
background: white;
|
||
border-radius: 6px;
|
||
border: 1px solid #ebeef5;
|
||
|
||
.file-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
flex-shrink: 0;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.file-details {
|
||
flex: 1;
|
||
|
||
.file-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.file-meta {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
|
||
span {
|
||
&:not(:last-child)::after {
|
||
content: '•';
|
||
margin-left: 8px;
|
||
color: #dcdfe6;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 预览抽屉样式
|
||
.preview-drawer {
|
||
::v-deep .el-drawer__header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-bottom: none;
|
||
margin-bottom: 0;
|
||
padding: 24px 32px;
|
||
color: white;
|
||
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.15);
|
||
|
||
.el-drawer__title {
|
||
font-weight: 700;
|
||
color: white;
|
||
font-size: 18px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.el-drawer__close-btn {
|
||
color: white;
|
||
font-size: 20px;
|
||
|
||
&:hover {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
}
|
||
|
||
::v-deep .el-drawer__body {
|
||
background: #fafbfc;
|
||
padding: 0;
|
||
}
|
||
}
|
||
|
||
// TXT 文件预览样式
|
||
.text-preview-container {
|
||
height: 100%;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
background: #fafafa;
|
||
|
||
.text-preview {
|
||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
color: #303133;
|
||
background: white;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
}
|
||
|
||
// Markdown 文件预览样式
|
||
.markdown-preview-container {
|
||
height: 100%;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
background: #fafafa;
|
||
|
||
.markdown-preview {
|
||
background: white;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
padding: 24px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
|
||
// Markdown 内容样式
|
||
h1, h2, h3, h4, h5, h6 {
|
||
margin-top: 0;
|
||
margin-bottom: 16px;
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
color: #24292e;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2em;
|
||
border-bottom: 1px solid #eaecef;
|
||
padding-bottom: 0.3em;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 1.5em;
|
||
border-bottom: 1px solid #eaecef;
|
||
padding-bottom: 0.3em;
|
||
}
|
||
|
||
h3 {
|
||
font-size: 1.25em;
|
||
}
|
||
|
||
p {
|
||
margin-bottom: 16px;
|
||
line-height: 1.6;
|
||
color: #24292e;
|
||
}
|
||
|
||
ul, ol {
|
||
padding-left: 2em;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
li {
|
||
margin-bottom: 0.25em;
|
||
}
|
||
|
||
blockquote {
|
||
padding: 0 1em;
|
||
color: #6a737d;
|
||
border-left: 0.25em solid #dfe2e5;
|
||
margin: 0 0 16px 0;
|
||
}
|
||
|
||
code {
|
||
padding: 0.2em 0.4em;
|
||
margin: 0;
|
||
font-size: 85%;
|
||
background-color: rgba(27, 31, 35, 0.05);
|
||
border-radius: 3px;
|
||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||
}
|
||
|
||
pre {
|
||
padding: 16px;
|
||
overflow: auto;
|
||
font-size: 85%;
|
||
line-height: 1.45;
|
||
background-color: #f6f8fa;
|
||
border-radius: 6px;
|
||
margin-bottom: 16px;
|
||
|
||
code {
|
||
display: inline;
|
||
max-width: auto;
|
||
padding: 0;
|
||
margin: 0;
|
||
overflow: visible;
|
||
line-height: inherit;
|
||
word-wrap: normal;
|
||
background-color: transparent;
|
||
border: 0;
|
||
}
|
||
}
|
||
|
||
table {
|
||
border-spacing: 0;
|
||
border-collapse: collapse;
|
||
margin-bottom: 16px;
|
||
width: 100%;
|
||
overflow: auto;
|
||
|
||
th, td {
|
||
padding: 6px 13px;
|
||
border: 1px solid #dfe2e5;
|
||
}
|
||
|
||
th {
|
||
font-weight: 600;
|
||
background-color: #f6f8fa;
|
||
}
|
||
|
||
tr:nth-child(2n) {
|
||
background-color: #f6f8fa;
|
||
}
|
||
}
|
||
|
||
img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
a {
|
||
color: #0366d6;
|
||
text-decoration: none;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
hr {
|
||
height: 0.25em;
|
||
padding: 0;
|
||
margin: 24px 0;
|
||
background-color: #e1e4e8;
|
||
border: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 状态标签动画
|
||
::v-deep .el-tag {
|
||
transition: all 0.3s ease;
|
||
|
||
.el-icon {
|
||
margin-right: 4px;
|
||
}
|
||
|
||
&.rotating-tag .el-icon.is-loading {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
// 不同状态的标签样式
|
||
&[type="info"] {
|
||
background-color: #f4f4f5;
|
||
border-color: #d3d4d6;
|
||
color: #909399;
|
||
}
|
||
|
||
&[type="warning"] {
|
||
background-color: #fdf6ec;
|
||
border-color: #f5dab1;
|
||
color: #e6a23c;
|
||
}
|
||
|
||
&[type="success"] {
|
||
background-color: #f0f9ff;
|
||
border-color: #b3d8ff;
|
||
color: #67c23a;
|
||
}
|
||
|
||
&[type="danger"] {
|
||
background-color: #fef0f0;
|
||
border-color: #fbc4c4;
|
||
color: #f56c6c;
|
||
}
|
||
}
|
||
|
||
// 深度解析对话框样式
|
||
.deep-analysis-dialog {
|
||
::v-deep .el-dialog__header {
|
||
background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
|
||
color: white;
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
|
||
::v-deep .el-dialog__title {
|
||
color: white;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
// 任务统计样式
|
||
.task-statistics {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-bottom: 20px;
|
||
|
||
.stat-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 16px;
|
||
background: linear-gradient(135deg, #f8f9ff 0%, #e8f4fd 100%);
|
||
border-radius: 8px;
|
||
border: 1px solid #e1e8f0;
|
||
|
||
.stat-number {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #409eff;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.task-list {
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.task-item {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 12px;
|
||
margin-bottom: 16px;
|
||
padding: 20px;
|
||
background: #fafafa;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||
transform: translateY(-3px);
|
||
}
|
||
|
||
&.completed {
|
||
background: linear-gradient(135deg, #f0f9ff 0%, #e6fffa 100%);
|
||
border-color: #67c23a;
|
||
}
|
||
}
|
||
|
||
.task-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 24px;
|
||
}
|
||
|
||
.task-info {
|
||
flex: 1;
|
||
|
||
.task-name {
|
||
margin: 0 0 12px 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.task-dataset, .task-id, .task-time {
|
||
margin: 6px 0;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.task-dataset {
|
||
color: #67c23a;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.task-progress {
|
||
flex: 0 0 220px;
|
||
|
||
.progress-status {
|
||
margin-top: 8px;
|
||
text-align: center;
|
||
|
||
.status-tag {
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
::v-deep .el-progress__text {
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
// 右键菜单样式
|
||
.context-menu {
|
||
position: fixed;
|
||
background: white;
|
||
border: none;
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||
padding: 0;
|
||
z-index: 3000;
|
||
min-width: 240px;
|
||
backdrop-filter: blur(20px);
|
||
overflow: hidden;
|
||
|
||
.context-menu-header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 16px 20px 12px;
|
||
border-bottom: none;
|
||
|
||
.menu-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
}
|
||
|
||
.context-menu-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
padding: 16px 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
border-bottom: 1px solid #f0f2f5;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&:hover {
|
||
background: linear-gradient(135deg, #f8fbff 0%, #e8f4fd 100%);
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
.item-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(135deg, #409eff 0%, #36cfc9 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.item-content {
|
||
flex: 1;
|
||
padding-top: 2px;
|
||
|
||
.item-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 4px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.item-desc {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.3;
|
||
}
|
||
}
|
||
|
||
&:hover .item-icon {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.3);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 右键菜单动画
|
||
.context-menu-fade-enter-active {
|
||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||
}
|
||
|
||
.context-menu-fade-leave-active {
|
||
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||
}
|
||
|
||
.context-menu-fade-enter-from {
|
||
opacity: 0;
|
||
transform: scale(0.8) translateY(-10px);
|
||
}
|
||
|
||
.context-menu-fade-leave-to {
|
||
opacity: 0;
|
||
transform: scale(0.9) translateY(-5px);
|
||
}
|
||
|
||
// 响应式设计
|
||
@media (max-width: 768px) {
|
||
.main-container {
|
||
padding: 12px;
|
||
}
|
||
|
||
|
||
.action-bar {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.left-group,
|
||
.right-group {
|
||
justify-content: center;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
}
|
||
.segment-list-container {
|
||
width: 30%;
|
||
border-right: 1px solid #ebeef5;
|
||
overflow-y: auto;
|
||
padding: 16px;
|
||
background: #f9fafc;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
h3 {
|
||
margin-bottom: 12px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.el-form-item {
|
||
cursor: pointer;
|
||
padding: 16px; // 内边距增加
|
||
border-radius: 8px;
|
||
background: white;
|
||
transition: all 0.2s ease;
|
||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||
min-height: 100px; // 增加最小高度,让卡片更高
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center; // 让内容垂直居中
|
||
|
||
&:hover {
|
||
background: #f0f4ff;
|
||
transform: translateX(2px);
|
||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
||
}
|
||
|
||
span {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #606266;
|
||
margin-top: 4px;
|
||
word-break: break-word; // 防止长文本溢出
|
||
}
|
||
|
||
h4 {
|
||
font-family: "Microsoft YaHei", "微软雅黑", sans-serif; // 设置微软雅黑字体
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #4a4a4a;
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 4;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
margin-top: 20px;
|
||
}
|
||
}
|
||
|
||
/* 右侧预览内容保持原有样式 */
|
||
.preview-content-container {
|
||
flex: 1;
|
||
padding: 10px;
|
||
overflow-y: auto;
|
||
background: #ffffff;
|
||
border-radius: 0 8px 8px 0;
|
||
}
|
||
|
||
|
||
|
||
</style>
|