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) => {
|
2025-07-29 15:16:51 +08:00
|
|
|
|
const icons = require.context('@/assets/img/filetype-icon', false, /\.png$/)
|
|
|
|
|
// console.log(icons)
|
|
|
|
|
// console.log('所有图标 keys:', icons.keys())
|
2025-07-28 14:53:59 +08:00
|
|
|
|
|
2025-07-29 15:16:51 +08:00
|
|
|
|
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,
|
2025-07-25 11:07:38 +08:00
|
|
|
|
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>
|