2025-08-05 18:00:45 +08:00
|
|
|
|
<template>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<a-modal v-model:open="visible" title="添加文件" width="800px" @cancel="handleCancel">
|
2025-08-05 18:00:45 +08:00
|
|
|
|
<template #footer>
|
2026-01-05 00:46:34 +08:00
|
|
|
|
<div class="footer-container">
|
|
|
|
|
|
<a-button type="link" class="help-link-btn" @click="openDocLink">
|
|
|
|
|
|
<CircleHelp :size="14" /> 文档处理说明
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<div class="footer-buttons">
|
|
|
|
|
|
<a-button key="back" @click="handleCancel">取消</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
key="submit"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="chunkData"
|
|
|
|
|
|
:loading="chunkLoading"
|
|
|
|
|
|
:disabled="fileList.length === 0"
|
|
|
|
|
|
>
|
|
|
|
|
|
添加到知识库
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-05 18:00:45 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="add-files-content">
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<!-- 1. 顶部操作栏 -->
|
|
|
|
|
|
<div class="top-action-bar">
|
|
|
|
|
|
<div class="mode-switch">
|
2025-11-06 21:59:48 +08:00
|
|
|
|
<a-segmented
|
|
|
|
|
|
v-model:value="uploadMode"
|
|
|
|
|
|
:options="uploadModeOptions"
|
2026-01-03 19:11:41 +08:00
|
|
|
|
class="custom-segmented"
|
2025-11-06 21:59:48 +08:00
|
|
|
|
/>
|
2025-08-05 18:00:45 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-05 00:46:34 +08:00
|
|
|
|
<!-- 2. 配置面板 -->
|
|
|
|
|
|
<div class="settings-panel">
|
2026-01-15 04:54:53 +08:00
|
|
|
|
<!-- 第一行:存储位置 + OCR 引擎 -->
|
|
|
|
|
|
<div class="setting-row two-cols">
|
|
|
|
|
|
<div class="col-item">
|
|
|
|
|
|
<div class="setting-label">存储位置</div>
|
|
|
|
|
|
<div class="setting-content flex-row">
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<a-tree-select
|
|
|
|
|
|
v-model:value="selectedFolderId"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
class="folder-select"
|
|
|
|
|
|
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
|
|
|
|
|
|
placeholder="选择目标文件夹(默认为根目录)"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
tree-default-expand-all
|
|
|
|
|
|
:tree-data="folderTreeData"
|
|
|
|
|
|
tree-node-filter-prop="title"
|
|
|
|
|
|
>
|
|
|
|
|
|
</a-tree-select>
|
2026-01-15 04:54:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<p class="param-description">选择文件保存的目标文件夹</p>
|
2026-01-03 19:11:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-item">
|
|
|
|
|
|
<div class="setting-label">
|
|
|
|
|
|
OCR 引擎
|
|
|
|
|
|
<a-tooltip title="检查服务状态">
|
|
|
|
|
|
<ReloadOutlined
|
|
|
|
|
|
class="action-icon refresh-icon"
|
|
|
|
|
|
:class="{ spinning: ocrHealthChecking }"
|
|
|
|
|
|
@click="checkOcrHealth"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="setting-content">
|
2025-08-05 18:00:45 +08:00
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="chunkParams.enable_ocr"
|
|
|
|
|
|
:options="enableOcrOptions"
|
2026-01-03 19:11:41 +08:00
|
|
|
|
style="width: 100%"
|
2025-08-05 18:00:45 +08:00
|
|
|
|
:disabled="ocrHealthChecking"
|
2026-01-03 19:11:41 +08:00
|
|
|
|
class="ocr-select"
|
2025-08-05 18:00:45 +08:00
|
|
|
|
/>
|
2026-01-15 04:54:53 +08:00
|
|
|
|
<p class="param-description">
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<template v-if="!isOcrEnabled"> 不启用 OCR,仅处理文本文件 </template>
|
2026-01-15 04:54:53 +08:00
|
|
|
|
<template v-else-if="selectedOcrStatus === 'healthy'">
|
|
|
|
|
|
{{ selectedOcrMessage || '服务正常' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="selectedOcrStatus === 'unknown'">
|
|
|
|
|
|
点击刷新图标检查服务状态
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
{{ selectedOcrMessage || '服务异常' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</p>
|
2026-01-03 19:11:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-12 17:51:32 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 第三行:自动入库配置 -->
|
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
|
<div class="col-item">
|
|
|
|
|
|
<div class="setting-label">
|
|
|
|
|
|
<a-checkbox v-model:checked="autoIndex">上传后自动入库</a-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="setting-content" v-if="autoIndex">
|
|
|
|
|
|
<template v-if="!isGraphBased">
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<ChunkParamsConfig :temp-chunk-params="indexParams" :show-qa-split="true" />
|
2026-01-12 17:51:32 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="lightrag-tip">
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<Info :size="14" style="margin-right: 6px" />
|
2026-01-12 17:51:32 +08:00
|
|
|
|
<span>LightRAG 将使用默认参数自动入库</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-05 18:00:45 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<!-- PDF/图片OCR提醒 (Alert样式优化) -->
|
2026-01-05 00:46:34 +08:00
|
|
|
|
<div v-if="hasPdfOrImageFiles && !isOcrEnabled" class="inline-alert warning">
|
|
|
|
|
|
<Info :size="16" />
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<span>检测到PDF或图片文件,建议启用 OCR 以提取文本内容</span>
|
2025-10-26 18:30:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
<!-- 文件上传区域 -->
|
2026-01-05 00:46:34 +08:00
|
|
|
|
<div class="upload-area">
|
2025-08-05 18:00:45 +08:00
|
|
|
|
<a-upload-dragger
|
2026-01-03 19:11:41 +08:00
|
|
|
|
class="custom-dragger"
|
2025-08-05 18:00:45 +08:00
|
|
|
|
v-model:fileList="fileList"
|
|
|
|
|
|
name="file"
|
|
|
|
|
|
:multiple="true"
|
2025-12-30 14:28:48 +08:00
|
|
|
|
:directory="isFolderUpload"
|
2025-08-05 18:00:45 +08:00
|
|
|
|
:disabled="chunkLoading"
|
2025-10-11 10:36:57 +08:00
|
|
|
|
:accept="acceptedFileTypes"
|
|
|
|
|
|
:before-upload="beforeUpload"
|
2025-12-30 14:28:48 +08:00
|
|
|
|
:customRequest="customRequest"
|
2025-08-05 18:00:45 +08:00
|
|
|
|
:action="'/api/knowledge/files/upload?db_id=' + databaseId"
|
|
|
|
|
|
:headers="getAuthHeaders()"
|
|
|
|
|
|
@change="handleFileUpload"
|
|
|
|
|
|
@drop="handleDrop"
|
|
|
|
|
|
>
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<p class="ant-upload-text">点击或将文件拖拽到此处</p>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<p class="ant-upload-hint">支持类型: {{ uploadHint }}</p>
|
|
|
|
|
|
<div class="zip-tip" v-if="hasZipFiles">📦 ZIP包将自动解压提取 Markdown 与图片</div>
|
2025-08-05 18:00:45 +08:00
|
|
|
|
</a-upload-dragger>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-07 23:38:20 +08:00
|
|
|
|
<!-- 同名文件提示 -->
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<div v-if="sameNameFiles.length > 0" class="conflict-files-panel">
|
|
|
|
|
|
<div class="panel-header">
|
2026-01-05 00:46:34 +08:00
|
|
|
|
<Info :size="14" class="icon-warning" />
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<span>已存在同名文件 ({{ sameNameFiles.length }})</span>
|
2025-12-07 23:38:20 +08:00
|
|
|
|
</div>
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<div class="file-list-scroll">
|
|
|
|
|
|
<div v-for="file in sameNameFiles" :key="file.file_id" class="conflict-item">
|
|
|
|
|
|
<div class="file-meta">
|
|
|
|
|
|
<span class="fname" :title="file.filename">{{ file.filename }}</span>
|
|
|
|
|
|
<span class="ftime">{{ formatFileTime(file.created_at) }}</span>
|
2025-12-07 23:38:20 +08:00
|
|
|
|
</div>
|
2026-01-03 19:11:41 +08:00
|
|
|
|
<div class="file-actions">
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="action-btn download"
|
|
|
|
|
|
@click="downloadSameNameFile(file)"
|
|
|
|
|
|
>
|
2026-01-05 00:46:34 +08:00
|
|
|
|
<Download :size="14" />
|
2025-12-07 23:38:20 +08:00
|
|
|
|
</a-button>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
danger
|
|
|
|
|
|
class="action-btn delete"
|
|
|
|
|
|
@click="deleteSameNameFile(file)"
|
|
|
|
|
|
>
|
2026-01-05 00:46:34 +08:00
|
|
|
|
<Trash2 :size="14" />
|
2025-12-07 23:38:20 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-05 18:00:45 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
|
|
|
|
import { message, Upload, Tooltip, Modal } from 'ant-design-vue'
|
|
|
|
|
|
import { useUserStore } from '@/stores/user'
|
|
|
|
|
|
import { useDatabaseStore } from '@/stores/database'
|
|
|
|
|
|
import { ocrApi } from '@/apis/system_api'
|
|
|
|
|
|
import { fileApi, documentApi } from '@/apis/knowledge_api'
|
|
|
|
|
|
import { CheckCircleFilled, ReloadOutlined } from '@ant-design/icons-vue'
|
|
|
|
|
|
import { FileUp, FolderUp, RotateCw, CircleHelp, Info, Download, Trash2 } from 'lucide-vue-next'
|
|
|
|
|
|
import { h } from 'vue'
|
|
|
|
|
|
import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue'
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
visible: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
2025-12-30 14:28:48 +08:00
|
|
|
|
folderTree: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
|
|
|
|
|
currentFolderId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: null
|
2026-01-04 22:47:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
isFolderMode: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
2025-12-30 14:28:48 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const emit = defineEmits(['update:visible', 'success'])
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const store = useDatabaseStore()
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-12-30 14:28:48 +08:00
|
|
|
|
// 文件夹选择相关
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const selectedFolderId = ref(null)
|
2025-12-30 14:28:48 +08:00
|
|
|
|
const folderTreeData = computed(() => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
// 转换 folderTree 数据为 TreeSelect 需要的格式
|
|
|
|
|
|
const transformData = (nodes) => {
|
|
|
|
|
|
return nodes
|
|
|
|
|
|
.map((node) => {
|
|
|
|
|
|
if (!node.is_folder) return null
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: node.filename,
|
|
|
|
|
|
value: node.file_id,
|
|
|
|
|
|
key: node.file_id,
|
|
|
|
|
|
children: node.children ? transformData(node.children).filter(Boolean) : []
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
return transformData(props.folderTree)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.visible,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
selectedFolderId.value = props.currentFolderId
|
|
|
|
|
|
isFolderUpload.value = props.isFolderMode
|
|
|
|
|
|
uploadMode.value = props.isFolderMode ? 'folder' : 'file'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SUPPORTED_TYPES = ['.txt', '.pdf', '.jpg', '.jpeg', '.md', '.docx']
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
const normalizeExtensions = (extensions) => {
|
|
|
|
|
|
if (!Array.isArray(extensions)) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return []
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
const normalized = extensions
|
|
|
|
|
|
.map((ext) => (typeof ext === 'string' ? ext.trim().toLowerCase() : ''))
|
|
|
|
|
|
.filter((ext) => ext.length > 0)
|
2026-01-15 06:01:34 +08:00
|
|
|
|
.map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return Array.from(new Set(normalized)).sort()
|
|
|
|
|
|
}
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const supportedFileTypes = ref(normalizeExtensions(DEFAULT_SUPPORTED_TYPES))
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
const applySupportedFileTypes = (extensions) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const normalized = normalizeExtensions(extensions)
|
2025-10-11 10:36:57 +08:00
|
|
|
|
if (normalized.length > 0) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
supportedFileTypes.value = normalized
|
2025-10-11 10:36:57 +08:00
|
|
|
|
} else {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
supportedFileTypes.value = normalizeExtensions(DEFAULT_SUPPORTED_TYPES)
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
const acceptedFileTypes = computed(() => {
|
|
|
|
|
|
if (!supportedFileTypes.value.length) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ''
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const exts = new Set(supportedFileTypes.value)
|
|
|
|
|
|
exts.add('.zip')
|
|
|
|
|
|
return Array.from(exts).join(',')
|
|
|
|
|
|
})
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
const uploadHint = computed(() => {
|
|
|
|
|
|
if (!supportedFileTypes.value.length) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return '加载中...'
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const exts = new Set(supportedFileTypes.value)
|
|
|
|
|
|
exts.add('.zip')
|
|
|
|
|
|
return Array.from(exts).join(', ')
|
|
|
|
|
|
})
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
const isSupportedExtension = (fileName) => {
|
|
|
|
|
|
if (!fileName) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return true
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!supportedFileTypes.value.length) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return true
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const lastDotIndex = fileName.lastIndexOf('.')
|
2025-10-11 10:36:57 +08:00
|
|
|
|
if (lastDotIndex === -1) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return false
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const ext = fileName.slice(lastDotIndex).toLowerCase()
|
|
|
|
|
|
return supportedFileTypes.value.includes(ext) || ext === '.zip'
|
|
|
|
|
|
}
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
const loadSupportedFileTypes = async () => {
|
|
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const data = await fileApi.getSupportedFileTypes()
|
|
|
|
|
|
applySupportedFileTypes(data?.file_types)
|
2025-10-11 10:36:57 +08:00
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
console.error('获取支持的文件类型失败:', error)
|
|
|
|
|
|
message.warning('获取支持的文件类型失败,已使用默认配置')
|
|
|
|
|
|
applySupportedFileTypes(DEFAULT_SUPPORTED_TYPES)
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
loadSupportedFileTypes()
|
|
|
|
|
|
})
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
const visible = computed({
|
|
|
|
|
|
get: () => props.visible,
|
|
|
|
|
|
set: (value) => emit('update:visible', value)
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const databaseId = computed(() => store.databaseId)
|
|
|
|
|
|
const kbType = computed(() => store.database.kb_type)
|
|
|
|
|
|
const chunkLoading = computed(() => store.state.chunkLoading)
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 上传模式
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const uploadMode = ref('file')
|
|
|
|
|
|
const previousOcrSelection = ref('disable')
|
2025-11-06 21:59:48 +08:00
|
|
|
|
|
|
|
|
|
|
const uploadModeOptions = computed(() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'file',
|
|
|
|
|
|
label: h('div', { class: 'segmented-option' }, [
|
2026-01-05 00:46:34 +08:00
|
|
|
|
h(FileUp, { size: 16, class: 'option-icon' }),
|
2026-01-15 06:01:34 +08:00
|
|
|
|
h('span', { class: 'option-text' }, '上传文件')
|
|
|
|
|
|
])
|
2025-11-06 21:59:48 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-01-05 00:46:34 +08:00
|
|
|
|
value: 'folder',
|
|
|
|
|
|
label: h('div', { class: 'segmented-option' }, [
|
|
|
|
|
|
h(FolderUp, { size: 16, class: 'option-icon' }),
|
2026-01-15 06:01:34 +08:00
|
|
|
|
h('span', { class: 'option-text' }, '上传文件夹')
|
|
|
|
|
|
])
|
|
|
|
|
|
}
|
|
|
|
|
|
])
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-05 00:46:34 +08:00
|
|
|
|
watch(uploadMode, (val) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
isFolderUpload.value = val === 'folder'
|
2026-01-05 00:46:34 +08:00
|
|
|
|
// 切换模式时清空已选文件,避免混淆
|
2026-01-15 06:01:34 +08:00
|
|
|
|
fileList.value = []
|
|
|
|
|
|
sameNameFiles.value = []
|
|
|
|
|
|
})
|
2026-01-05 00:46:34 +08:00
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
// 文件列表
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const fileList = ref([])
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-12-07 23:38:20 +08:00
|
|
|
|
// 同名文件列表(用于显示提示)
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const sameNameFiles = ref([])
|
2025-11-15 12:18:31 +08:00
|
|
|
|
|
2025-11-23 14:57:38 +08:00
|
|
|
|
// URL相关功能已移除
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
// OCR服务健康状态
|
|
|
|
|
|
const ocrHealthStatus = ref({
|
2025-10-25 14:26:47 +08:00
|
|
|
|
onnx_rapid_ocr: { status: 'unknown', message: '' },
|
2025-08-05 18:00:45 +08:00
|
|
|
|
mineru_ocr: { status: 'unknown', message: '' },
|
2025-10-25 14:26:47 +08:00
|
|
|
|
mineru_official: { status: 'unknown', message: '' },
|
2025-12-29 10:32:07 +08:00
|
|
|
|
paddlex_ocr: { status: 'unknown', message: '' },
|
|
|
|
|
|
deepseek_ocr: { status: 'unknown', message: '' }
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
// OCR健康检查状态
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const ocrHealthChecking = ref(false)
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 分块参数
|
|
|
|
|
|
const chunkParams = ref({
|
2026-01-15 06:01:34 +08:00
|
|
|
|
enable_ocr: 'disable'
|
|
|
|
|
|
})
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-12 17:51:32 +08:00
|
|
|
|
// 自动入库相关
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const autoIndex = ref(false)
|
2026-01-12 17:51:32 +08:00
|
|
|
|
const indexParams = ref({
|
|
|
|
|
|
chunk_size: 1000,
|
|
|
|
|
|
chunk_overlap: 200,
|
|
|
|
|
|
qa_separator: ''
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2026-01-12 17:51:32 +08:00
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
// 计算属性:是否支持QA分割
|
|
|
|
|
|
const isQaSplitSupported = computed(() => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const type = kbType.value?.toLowerCase()
|
|
|
|
|
|
return type === 'milvus'
|
|
|
|
|
|
})
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-08-12 19:14:44 +08:00
|
|
|
|
const isGraphBased = computed(() => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const type = kbType.value?.toLowerCase()
|
|
|
|
|
|
return type === 'lightrag'
|
|
|
|
|
|
})
|
2025-08-12 19:14:44 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const isFolderUpload = ref(false)
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
2025-10-26 18:30:59 +08:00
|
|
|
|
// 计算属性:是否启用了OCR
|
|
|
|
|
|
const isOcrEnabled = computed(() => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return chunkParams.value.enable_ocr !== 'disable'
|
|
|
|
|
|
})
|
2025-10-26 18:30:59 +08:00
|
|
|
|
|
2025-11-23 14:57:38 +08:00
|
|
|
|
// 上传模式切换相关逻辑已移除
|
2025-11-06 21:59:48 +08:00
|
|
|
|
|
2025-10-26 18:30:59 +08:00
|
|
|
|
// 计算属性:是否有PDF或图片文件
|
|
|
|
|
|
const hasPdfOrImageFiles = computed(() => {
|
|
|
|
|
|
if (fileList.value.length === 0) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return false
|
2025-10-26 18:30:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const pdfExtensions = ['.pdf']
|
|
|
|
|
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.gif', '.webp']
|
|
|
|
|
|
const ocrExtensions = [...pdfExtensions, ...imageExtensions]
|
2025-10-26 18:30:59 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return fileList.value.some((file) => {
|
2025-10-26 18:30:59 +08:00
|
|
|
|
if (file.status !== 'done') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return false
|
2025-10-26 18:30:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const filePath = file.response?.file_path || file.name
|
2025-10-26 18:30:59 +08:00
|
|
|
|
if (!filePath) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return false
|
2025-10-26 18:30:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase()
|
|
|
|
|
|
return ocrExtensions.includes(ext)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2025-10-26 18:30:59 +08:00
|
|
|
|
|
2025-11-15 12:18:31 +08:00
|
|
|
|
// 计算属性:是否有ZIP文件
|
|
|
|
|
|
const hasZipFiles = computed(() => {
|
|
|
|
|
|
if (fileList.value.length === 0) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return false
|
2025-11-15 12:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return fileList.value.some((file) => {
|
2025-11-15 12:18:31 +08:00
|
|
|
|
if (file.status !== 'done') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return false
|
2025-11-15 12:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const filePath = file.response?.file_path || file.name
|
2025-11-15 12:18:31 +08:00
|
|
|
|
if (!filePath) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return false
|
2025-11-15 12:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase()
|
|
|
|
|
|
return ext === '.zip'
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2025-11-15 12:18:31 +08:00
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
// 计算属性:OCR选项
|
|
|
|
|
|
const enableOcrOptions = computed(() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'disable',
|
|
|
|
|
|
label: '不启用',
|
|
|
|
|
|
title: '不启用'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'onnx_rapid_ocr',
|
|
|
|
|
|
label: getRapidOcrLabel(),
|
|
|
|
|
|
title: 'ONNX with RapidOCR',
|
2026-01-15 06:01:34 +08:00
|
|
|
|
disabled:
|
|
|
|
|
|
ocrHealthStatus.value?.onnx_rapid_ocr?.status === 'unavailable' ||
|
|
|
|
|
|
ocrHealthStatus.value?.onnx_rapid_ocr?.status === 'error'
|
2025-08-05 18:00:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'mineru_ocr',
|
|
|
|
|
|
label: getMinerULabel(),
|
|
|
|
|
|
title: 'MinerU OCR',
|
2026-01-15 06:01:34 +08:00
|
|
|
|
disabled:
|
|
|
|
|
|
ocrHealthStatus.value?.mineru_ocr?.status === 'unavailable' ||
|
|
|
|
|
|
ocrHealthStatus.value?.mineru_ocr?.status === 'error'
|
2025-10-25 14:26:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'mineru_official',
|
|
|
|
|
|
label: getMinerUOfficialLabel(),
|
|
|
|
|
|
title: 'MinerU Official API',
|
2026-01-15 06:01:34 +08:00
|
|
|
|
disabled:
|
|
|
|
|
|
ocrHealthStatus.value?.mineru_official?.status === 'unavailable' ||
|
|
|
|
|
|
ocrHealthStatus.value?.mineru_official?.status === 'error'
|
2025-08-05 18:00:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'paddlex_ocr',
|
|
|
|
|
|
label: getPaddleXLabel(),
|
2025-12-30 19:40:09 +08:00
|
|
|
|
title: 'PP-StructureV3',
|
2026-01-15 06:01:34 +08:00
|
|
|
|
disabled:
|
|
|
|
|
|
ocrHealthStatus.value?.paddlex_ocr?.status === 'unavailable' ||
|
|
|
|
|
|
ocrHealthStatus.value?.paddlex_ocr?.status === 'error'
|
2025-08-05 18:00:45 +08:00
|
|
|
|
},
|
2025-12-29 10:32:07 +08:00
|
|
|
|
{
|
|
|
|
|
|
value: 'deepseek_ocr',
|
|
|
|
|
|
label: getDeepSeekOcrLabel(),
|
|
|
|
|
|
title: 'DeepSeek OCR (SiliconFlow)',
|
2026-01-15 06:01:34 +08:00
|
|
|
|
disabled:
|
|
|
|
|
|
ocrHealthStatus.value?.deepseek_ocr?.status === 'unavailable' ||
|
|
|
|
|
|
ocrHealthStatus.value?.deepseek_ocr?.status === 'error'
|
|
|
|
|
|
}
|
|
|
|
|
|
])
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取当前选中OCR服务的状态
|
|
|
|
|
|
const selectedOcrStatus = computed(() => {
|
|
|
|
|
|
switch (chunkParams.value.enable_ocr) {
|
|
|
|
|
|
case 'onnx_rapid_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.onnx_rapid_ocr?.status || 'unknown'
|
2025-08-05 18:00:45 +08:00
|
|
|
|
case 'mineru_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.mineru_ocr?.status || 'unknown'
|
2025-10-25 14:26:47 +08:00
|
|
|
|
case 'mineru_official':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.mineru_official?.status || 'unknown'
|
2025-08-05 18:00:45 +08:00
|
|
|
|
case 'paddlex_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.paddlex_ocr?.status || 'unknown'
|
2025-12-29 10:32:07 +08:00
|
|
|
|
case 'deepseek_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.deepseek_ocr?.status || 'unknown'
|
2025-08-05 18:00:45 +08:00
|
|
|
|
default:
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return null
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取当前选中OCR服务的状态消息
|
|
|
|
|
|
const selectedOcrMessage = computed(() => {
|
|
|
|
|
|
switch (chunkParams.value.enable_ocr) {
|
|
|
|
|
|
case 'onnx_rapid_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.onnx_rapid_ocr?.message || ''
|
2025-08-05 18:00:45 +08:00
|
|
|
|
case 'mineru_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.mineru_ocr?.message || ''
|
2025-10-25 14:26:47 +08:00
|
|
|
|
case 'mineru_official':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.mineru_official?.message || ''
|
2025-08-05 18:00:45 +08:00
|
|
|
|
case 'paddlex_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.paddlex_ocr?.message || ''
|
2025-12-29 10:32:07 +08:00
|
|
|
|
case 'deepseek_ocr':
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ocrHealthStatus.value?.deepseek_ocr?.message || ''
|
2025-08-05 18:00:45 +08:00
|
|
|
|
default:
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return ''
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-12-29 10:32:07 +08:00
|
|
|
|
// OCR服务状态图标映射
|
|
|
|
|
|
const STATUS_ICONS = {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
healthy: '✅',
|
|
|
|
|
|
unavailable: '❌',
|
|
|
|
|
|
unhealthy: '⚠️',
|
|
|
|
|
|
timeout: '⏰',
|
|
|
|
|
|
error: '⚠️',
|
|
|
|
|
|
unknown: '❓'
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-12-29 10:32:07 +08:00
|
|
|
|
// OCR选项标签生成通用函数
|
|
|
|
|
|
const getOcrLabel = (serviceKey, displayName) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const status = ocrHealthStatus.value?.[serviceKey]?.status || 'unknown'
|
|
|
|
|
|
return `${STATUS_ICONS[status] || '❓'} ${displayName}`
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-12-29 10:32:07 +08:00
|
|
|
|
// 兼容性包装器
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const getRapidOcrLabel = () => getOcrLabel('onnx_rapid_ocr', 'RapidOCR (ONNX)')
|
|
|
|
|
|
const getMinerULabel = () => getOcrLabel('mineru_ocr', 'MinerU OCR')
|
|
|
|
|
|
const getMinerUOfficialLabel = () => getOcrLabel('mineru_official', 'MinerU Official API')
|
|
|
|
|
|
const getPaddleXLabel = () => getOcrLabel('paddlex_ocr', 'PP-StructureV3')
|
|
|
|
|
|
const getDeepSeekOcrLabel = () => getOcrLabel('deepseek_ocr', 'DeepSeek OCR')
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证OCR服务可用性
|
|
|
|
|
|
const validateOcrService = () => {
|
|
|
|
|
|
if (chunkParams.value.enable_ocr === 'disable') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return true
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const status = selectedOcrStatus.value
|
2025-08-05 18:00:45 +08:00
|
|
|
|
if (status === 'unavailable' || status === 'error') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const ocrMessage = selectedOcrMessage.value
|
|
|
|
|
|
message.error(`OCR服务不可用: ${ocrMessage}`)
|
|
|
|
|
|
return false
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return true
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
emit('update:visible', false)
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-10-11 10:36:57 +08:00
|
|
|
|
const beforeUpload = (file) => {
|
|
|
|
|
|
if (!isSupportedExtension(file?.name)) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.error(`不支持的文件类型:${file?.name || '未知文件'}`)
|
|
|
|
|
|
return Upload.LIST_IGNORE
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return true
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-12-07 23:38:20 +08:00
|
|
|
|
const formatFileSize = (bytes) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (bytes === 0 || !bytes) 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(1))} ${sizes[i]}`
|
|
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
const formatFileTime = (timestamp) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (!timestamp) return ''
|
2025-12-07 23:38:20 +08:00
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const date = new Date(timestamp)
|
|
|
|
|
|
return date.toLocaleString()
|
2025-12-07 23:38:20 +08:00
|
|
|
|
} catch (e) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return timestamp
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
const showSameNameFilesInUploadArea = (files) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
sameNameFiles.value = files
|
2025-12-07 23:38:20 +08:00
|
|
|
|
// 可以在这里添加其他逻辑,比如自动滚动到提示区域
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
const downloadSameNameFile = async (file) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取当前数据库ID
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const currentDbId = databaseId.value
|
2025-12-07 23:38:20 +08:00
|
|
|
|
if (!currentDbId) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.error('知识库ID不存在')
|
|
|
|
|
|
return
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.loading('正在下载文件...', 0)
|
|
|
|
|
|
const response = await documentApi.downloadDocument(currentDbId, file.file_id)
|
|
|
|
|
|
message.destroy()
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建下载链接
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const blob = await response.blob() // 从 Response 对象中提取 Blob 数据
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob)
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
link.href = url
|
|
|
|
|
|
link.download = file.filename
|
|
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
window.URL.revokeObjectURL(url)
|
|
|
|
|
|
|
|
|
|
|
|
message.success(`文件 ${file.filename} 下载成功`)
|
2025-12-07 23:38:20 +08:00
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.destroy()
|
|
|
|
|
|
console.error('下载文件失败:', error)
|
|
|
|
|
|
message.error(`下载文件失败: ${error.message || '未知错误'}`)
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
const deleteSameNameFile = (file) => {
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认删除文件',
|
|
|
|
|
|
content: `确定要删除文件 "${file.filename}" 吗?此操作不可恢复。`,
|
|
|
|
|
|
okText: '删除',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取当前数据库ID
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const currentDbId = databaseId.value
|
2025-12-07 23:38:20 +08:00
|
|
|
|
if (!currentDbId) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.error('知识库ID不存在')
|
|
|
|
|
|
return
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.loading('正在删除文件...', 0)
|
|
|
|
|
|
await documentApi.deleteDocument(currentDbId, file.file_id)
|
|
|
|
|
|
message.destroy()
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 从同名文件列表中移除
|
2026-01-15 06:01:34 +08:00
|
|
|
|
sameNameFiles.value = sameNameFiles.value.filter((f) => f.file_id !== file.file_id)
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.success(`文件 ${file.filename} 删除成功`)
|
2025-12-07 23:38:20 +08:00
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.destroy()
|
|
|
|
|
|
console.error('删除文件失败:', error)
|
|
|
|
|
|
message.error(`删除文件失败: ${error.message || '未知错误'}`)
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
2025-12-30 14:28:48 +08:00
|
|
|
|
const customRequest = async (options) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const { file, onProgress, onSuccess, onError } = options
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const formData = new FormData()
|
2025-12-30 14:28:48 +08:00
|
|
|
|
// 如果是文件夹上传,使用相对路径作为文件名
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const filename =
|
|
|
|
|
|
isFolderUpload.value && file.webkitRelativePath ? file.webkitRelativePath : file.name
|
|
|
|
|
|
formData.append('file', file, filename)
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const dbId = databaseId.value
|
2025-12-30 14:28:48 +08:00
|
|
|
|
if (!dbId) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
onError(new Error('Database ID is missing'))
|
|
|
|
|
|
return
|
2025-12-30 14:28:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const xhr = new XMLHttpRequest()
|
|
|
|
|
|
xhr.open('POST', `/api/knowledge/files/upload?db_id=${dbId}`)
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const headers = getAuthHeaders()
|
2025-12-30 14:28:48 +08:00
|
|
|
|
for (const [key, value] of Object.entries(headers)) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
xhr.setRequestHeader(key, value)
|
2025-12-30 14:28:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
xhr.upload.onprogress = (e) => {
|
|
|
|
|
|
if (e.lengthComputable) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
onProgress({ percent: (e.loaded / e.total) * 100 })
|
2025-12-30 14:28:48 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
|
|
|
|
|
xhr.onload = () => {
|
|
|
|
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const response = JSON.parse(xhr.responseText)
|
|
|
|
|
|
onSuccess(response, xhr)
|
2025-12-30 14:28:48 +08:00
|
|
|
|
} catch (e) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
onError(e)
|
2025-12-30 14:28:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const errorResp = JSON.parse(xhr.responseText)
|
|
|
|
|
|
onError(new Error(errorResp.detail || 'Upload failed'))
|
2025-12-30 14:28:48 +08:00
|
|
|
|
} catch (e) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
onError(new Error(xhr.responseText || 'Upload failed'))
|
2025-12-30 14:28:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
|
|
|
|
|
xhr.onerror = (e) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
onError(e)
|
|
|
|
|
|
}
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
xhr.send(formData)
|
|
|
|
|
|
}
|
2025-12-30 14:28:48 +08:00
|
|
|
|
|
2025-10-11 10:36:57 +08:00
|
|
|
|
const handleFileUpload = (info) => {
|
|
|
|
|
|
if (info?.file?.status === 'error') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const errorMessage = info.file?.response?.detail || `文件上传失败:${info.file.name}`
|
|
|
|
|
|
message.error(errorMessage)
|
2025-10-11 10:36:57 +08:00
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否有同名文件提示
|
|
|
|
|
|
if (info?.file?.status === 'done' && info.file.response) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const response = info.file.response
|
2025-12-07 23:38:20 +08:00
|
|
|
|
if (response.has_same_name && response.same_name_files && response.same_name_files.length > 0) {
|
|
|
|
|
|
// 显示同名文件提示
|
2026-01-15 06:01:34 +08:00
|
|
|
|
showSameNameFilesInUploadArea(response.same_name_files)
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
fileList.value = info?.fileList ?? []
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const handleDrop = () => {}
|
2025-10-11 10:36:57 +08:00
|
|
|
|
|
2025-11-15 12:18:31 +08:00
|
|
|
|
// 已移除文件夹上传逻辑
|
|
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
const checkOcrHealth = async () => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (ocrHealthChecking.value) return
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
ocrHealthChecking.value = true
|
2025-08-05 18:00:45 +08:00
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const healthData = await ocrApi.getHealth()
|
|
|
|
|
|
ocrHealthStatus.value = healthData.services
|
2025-08-05 18:00:45 +08:00
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
console.error('OCR健康检查失败:', error)
|
|
|
|
|
|
message.error('OCR服务健康检查失败')
|
2025-08-05 18:00:45 +08:00
|
|
|
|
} finally {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
ocrHealthChecking.value = false
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
|
|
|
|
|
const getAuthHeaders = () => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
return userStore.getAuthHeaders()
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2025-12-14 18:13:46 +08:00
|
|
|
|
const openDocLink = () => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
window.open(
|
|
|
|
|
|
'https://xerrors.github.io/Yuxi-Know/latest/advanced/document-processing.html',
|
|
|
|
|
|
'_blank',
|
|
|
|
|
|
'noopener'
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-14 18:13:46 +08:00
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
const chunkData = async () => {
|
2025-11-15 12:18:31 +08:00
|
|
|
|
if (!databaseId.value) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.error('请先选择知识库')
|
|
|
|
|
|
return
|
2025-11-15 12:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
// 验证OCR服务可用性
|
|
|
|
|
|
if (!validateOcrService()) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
let success = false
|
|
|
|
|
|
const files = fileList.value
|
|
|
|
|
|
.filter((file) => file.status === 'done')
|
|
|
|
|
|
.map((file) => file.response?.file_path)
|
2026-01-05 00:46:34 +08:00
|
|
|
|
// 过滤掉 undefined 或 null 的文件路径
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const validFiles = files.filter((file) => file)
|
2026-01-05 00:46:34 +08:00
|
|
|
|
if (validFiles.length === 0) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.error('请先上传文件')
|
|
|
|
|
|
return
|
2026-01-05 00:46:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证图片文件是否启用OCR
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif']
|
|
|
|
|
|
const hasImageFiles = validFiles.some((filePath) => {
|
|
|
|
|
|
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase()
|
|
|
|
|
|
return imageExtensions.includes(ext)
|
|
|
|
|
|
})
|
2025-10-25 14:26:47 +08:00
|
|
|
|
|
2026-01-05 00:46:34 +08:00
|
|
|
|
if (hasImageFiles && chunkParams.value.enable_ocr === 'disable') {
|
|
|
|
|
|
message.error({
|
2026-01-15 06:01:34 +08:00
|
|
|
|
content:
|
|
|
|
|
|
'检测到图片文件,必须启用 OCR 才能提取文本内容。请在上方选择 OCR 方式 (RapidOCR/MinerU/MinerU Official/PP-StructureV3) 或移除图片文件。',
|
|
|
|
|
|
duration: 5
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
2026-01-05 00:46:34 +08:00
|
|
|
|
}
|
2025-10-25 14:26:47 +08:00
|
|
|
|
|
2026-01-05 00:46:34 +08:00
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
store.state.chunkLoading = true
|
2026-01-12 17:51:32 +08:00
|
|
|
|
// 构建参数
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const params = { ...chunkParams.value }
|
2026-01-12 17:51:32 +08:00
|
|
|
|
if (autoIndex.value) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
params.auto_index = true
|
|
|
|
|
|
Object.assign(params, indexParams.value)
|
2026-01-12 17:51:32 +08:00
|
|
|
|
}
|
2026-01-05 00:46:34 +08:00
|
|
|
|
// 调用 store 的 addFiles 方法
|
|
|
|
|
|
await store.addFiles({
|
|
|
|
|
|
items: validFiles,
|
|
|
|
|
|
contentType: 'file',
|
2026-01-12 17:51:32 +08:00
|
|
|
|
params,
|
2026-01-05 00:46:34 +08:00
|
|
|
|
parentId: selectedFolderId.value // 传递选中的文件夹 ID
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-10-25 14:26:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
emit('success')
|
|
|
|
|
|
handleCancel()
|
|
|
|
|
|
fileList.value = []
|
|
|
|
|
|
sameNameFiles.value = []
|
|
|
|
|
|
success = true
|
2026-01-05 00:46:34 +08:00
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
console.error('文件上传失败:', error)
|
|
|
|
|
|
message.error('文件上传失败: ' + (error.message || '未知错误'))
|
2026-01-05 00:46:34 +08:00
|
|
|
|
} finally {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
store.state.chunkLoading = false
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
emit('update:visible', false)
|
|
|
|
|
|
emit('success')
|
|
|
|
|
|
fileList.value = []
|
|
|
|
|
|
sameNameFiles.value = [] // 清空同名文件列表
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-08-05 22:58:30 +08:00
|
|
|
|
<style lang="less" scoped>
|
2026-01-05 00:46:34 +08:00
|
|
|
|
.footer-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 18:00:45 +08:00
|
|
|
|
.add-files-content {
|
2026-01-03 19:11:41 +08:00
|
|
|
|
padding: 8px 0;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
gap: 16px;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
/* Top Bar */
|
|
|
|
|
|
.top-action-bar {
|
2025-08-05 18:00:45 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.help-link-btn {
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
font-size: 13px;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
display: flex;
|
2025-11-06 21:59:48 +08:00
|
|
|
|
align-items: center;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
gap: 6px;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
2025-12-19 00:41:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.custom-segmented {
|
|
|
|
|
|
background-color: var(--gray-100);
|
|
|
|
|
|
padding: 3px;
|
|
|
|
|
|
|
2026-01-05 00:46:34 +08:00
|
|
|
|
.segmented-option {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
.option-text {
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
}
|
2026-01-03 19:11:41 +08:00
|
|
|
|
}
|
2025-11-06 21:59:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
/* Settings Panel */
|
|
|
|
|
|
.settings-panel {
|
|
|
|
|
|
background-color: var(--gray-50);
|
2025-11-06 21:59:48 +08:00
|
|
|
|
border: 1px solid var(--gray-200);
|
2026-01-03 19:11:41 +08:00
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16px;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.setting-row {
|
2025-11-06 21:59:48 +08:00
|
|
|
|
display: flex;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
flex-direction: column;
|
2025-11-06 21:59:48 +08:00
|
|
|
|
gap: 8px;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
&.two-cols {
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
gap: 20px;
|
2026-01-05 00:46:34 +08:00
|
|
|
|
}
|
2026-01-03 19:11:41 +08:00
|
|
|
|
|
2026-01-05 00:46:34 +08:00
|
|
|
|
.col-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-01-15 04:54:53 +08:00
|
|
|
|
gap: 4px;
|
2026-01-05 00:46:34 +08:00
|
|
|
|
min-width: 0; // Fix flex overflow
|
2026-01-03 19:11:41 +08:00
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.setting-label {
|
2025-11-07 15:01:59 +08:00
|
|
|
|
font-size: 13px;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--gray-700);
|
2025-08-05 18:00:45 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-11-06 21:59:48 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.action-icon {
|
|
|
|
|
|
color: var(--gray-400);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
2025-12-19 00:41:51 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
&.spinning {
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
color: var(--main-color);
|
2025-12-19 00:41:51 +08:00
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
@keyframes spin {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
from {
|
|
|
|
|
|
transform: rotate(0deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.flex-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
width: 100%;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.folder-select {
|
|
|
|
|
|
flex: 1;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.folder-checkbox {
|
|
|
|
|
|
margin-left: 12px;
|
|
|
|
|
|
white-space: nowrap;
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 04:54:53 +08:00
|
|
|
|
.param-description {
|
2025-08-05 18:00:45 +08:00
|
|
|
|
font-size: 12px;
|
2026-01-15 04:54:53 +08:00
|
|
|
|
color: var(--gray-400);
|
|
|
|
|
|
margin: 4px 0 0 0;
|
|
|
|
|
|
line-height: 1.4;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
2026-01-15 04:54:53 +08:00
|
|
|
|
.text-success {
|
|
|
|
|
|
color: var(--color-success-500);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.text-warning {
|
|
|
|
|
|
color: var(--color-warning-500);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
/* Chunk Display Card */
|
|
|
|
|
|
.chunk-display-card {
|
|
|
|
|
|
background: var(--gray-0);
|
|
|
|
|
|
border: 1px solid var(--gray-300);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 0 12px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
box-shadow: 0 0 0 2px var(--main-100);
|
|
|
|
|
|
|
|
|
|
|
|
.edit-icon {
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.disabled {
|
|
|
|
|
|
background: var(--gray-100);
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
color: var(--gray-400);
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: var(--gray-300);
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-05 18:00:45 +08:00
|
|
|
|
}
|
2025-10-26 18:30:59 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.chunk-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--gray-700);
|
|
|
|
|
|
|
|
|
|
|
|
.divider {
|
|
|
|
|
|
color: var(--gray-300);
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
b {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--gray-900);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-icon {
|
|
|
|
|
|
color: var(--gray-400);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Alerts */
|
|
|
|
|
|
.inline-alert {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
2025-10-26 18:30:59 +08:00
|
|
|
|
padding: 8px 12px;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
border-radius: 6px;
|
2025-10-26 18:30:59 +08:00
|
|
|
|
font-size: 13px;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
|
|
|
|
|
|
&.warning {
|
|
|
|
|
|
background: var(--color-warning-50);
|
|
|
|
|
|
border: 1px solid var(--color-warning-200);
|
|
|
|
|
|
color: var(--color-warning-700);
|
|
|
|
|
|
}
|
2025-10-26 18:30:59 +08:00
|
|
|
|
}
|
2025-11-15 12:18:31 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
/* Upload Area */
|
|
|
|
|
|
.upload-area {
|
|
|
|
|
|
flex: 1;
|
2025-11-15 12:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.custom-dragger {
|
|
|
|
|
|
:deep(.ant-upload-drag) {
|
|
|
|
|
|
background: var(--gray-0);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px dashed var(--gray-300);
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
background: var(--main-50);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-upload-drag-icon {
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
color: var(--main-300);
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-upload-text {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-upload-hint {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
}
|
2025-11-15 12:18:31 +08:00
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.zip-tip {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--color-warning-600);
|
|
|
|
|
|
background: var(--color-warning-50);
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
2025-12-14 18:13:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
/* Conflict Files Panel */
|
|
|
|
|
|
.conflict-files-panel {
|
|
|
|
|
|
border: 1px solid var(--gray-200);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: var(--gray-0);
|
|
|
|
|
|
margin-top: 4px;
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.panel-header {
|
|
|
|
|
|
background: var(--gray-50);
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--gray-700);
|
2025-12-07 23:38:20 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
border-bottom: 1px solid var(--gray-200);
|
|
|
|
|
|
|
|
|
|
|
|
.icon-warning {
|
|
|
|
|
|
color: var(--color-warning-500);
|
|
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.file-list-scroll {
|
|
|
|
|
|
max-height: 120px;
|
|
|
|
|
|
overflow-y: auto;
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.conflict-item {
|
2025-12-07 23:38:20 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 8px 12px;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
border-bottom: 1px solid var(--gray-100);
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: var(--gray-50);
|
|
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.file-meta {
|
2025-12-07 23:38:20 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
font-size: 13px;
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.fname {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.ftime {
|
|
|
|
|
|
color: var(--gray-400);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.file-actions {
|
2025-12-07 23:38:20 +08:00
|
|
|
|
display: flex;
|
2026-01-03 19:11:41 +08:00
|
|
|
|
gap: 4px;
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
.action-btn {
|
|
|
|
|
|
color: var(--gray-500);
|
2025-12-07 23:38:20 +08:00
|
|
|
|
|
2026-01-03 19:11:41 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
color: var(--main-600);
|
|
|
|
|
|
background: var(--main-50);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.delete:hover {
|
|
|
|
|
|
color: var(--color-error-500);
|
|
|
|
|
|
background: var(--color-error-50);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-07 23:38:20 +08:00
|
|
|
|
}
|
2026-01-12 17:51:32 +08:00
|
|
|
|
|
|
|
|
|
|
.auto-index-params {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
background: var(--gray-0);
|
|
|
|
|
|
border: 1px solid var(--gray-200);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lightrag-tip {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
background: var(--main-50);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-label .ant-checkbox {
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
2025-10-11 10:36:57 +08:00
|
|
|
|
</style>
|