feat(FileUploadModal): 添加文件夹上传模式并优化图标库
refactor(KnowledgeBaseTool): 替换ant-design图标为lucide-vue-next style: 调整多个组件的灰色调色板变量 fix(FileTable): 移除分页配置并调整悬浮延迟时间
This commit is contained in:
parent
88aecfbebc
commit
3812d61566
@ -177,7 +177,7 @@
|
||||
class="my-table"
|
||||
size="small"
|
||||
:show-header="false"
|
||||
:pagination="paginationCompact"
|
||||
:pagination="false"
|
||||
v-model:expandedRowKeys="expandedRowKeys"
|
||||
:custom-row="customRow"
|
||||
:row-selection="isSelectionMode ? {
|
||||
@ -196,7 +196,7 @@
|
||||
{{ record.filename }}
|
||||
</span>
|
||||
</template>
|
||||
<a-popover v-else placement="right" overlayClassName="file-info-popover" :mouseEnterDelay="1">
|
||||
<a-popover v-else placement="right" overlayClassName="file-info-popover" :mouseEnterDelay="0.5">
|
||||
<template #content>
|
||||
<div class="file-info-card">
|
||||
<div class="info-row"><span class="label">ID:</span> <span class="value">{{ record.file_id }}</span></div>
|
||||
@ -721,27 +721,6 @@ const emptyText = computed(() => {
|
||||
return filenameFilter.value ? `没有找到包含"${filenameFilter.value}"的文件` : '暂无文件';
|
||||
});
|
||||
|
||||
// 紧凑分页配置
|
||||
const paginationCompact = ref({
|
||||
pageSize: 20,
|
||||
current: 1,
|
||||
total: 0,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `${total}`,
|
||||
size: 'small',
|
||||
showQuickJumper: false,
|
||||
onChange: (page, pageSize) => {
|
||||
paginationCompact.value.current = page;
|
||||
paginationCompact.value.pageSize = pageSize;
|
||||
selectedRowKeys.value = [];
|
||||
},
|
||||
});
|
||||
|
||||
// 监听过滤后的文件列表变化,更新分页总数
|
||||
watch(filteredFiles, (newFiles) => {
|
||||
paginationCompact.value.total = newFiles.length;
|
||||
}, { immediate: true });
|
||||
|
||||
// 计算是否可以批量删除
|
||||
const canBatchDelete = computed(() => {
|
||||
return selectedRowKeys.value.some(key => {
|
||||
|
||||
@ -6,16 +6,23 @@
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<template #footer>
|
||||
<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 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>
|
||||
</template>
|
||||
|
||||
<div class="add-files-content">
|
||||
@ -26,16 +33,12 @@
|
||||
v-model:value="uploadMode"
|
||||
:options="uploadModeOptions"
|
||||
class="custom-segmented"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<a-button type="link" class="help-link-btn" @click="openDocLink">
|
||||
<QuestionCircleOutlined /> 文档处理说明
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 2. 配置面板 (仅文件模式显示) -->
|
||||
<div class="settings-panel" v-if="uploadMode === 'file'">
|
||||
<!-- 2. 配置面板 -->
|
||||
<div class="settings-panel">
|
||||
<!-- 第一行:存储位置 -->
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">存储位置</div>
|
||||
@ -80,7 +83,7 @@
|
||||
<!-- 紧凑的状态提示 -->
|
||||
<div class="status-mini-tip" v-if="chunkParams.enable_ocr !== 'disable'">
|
||||
<span v-if="selectedOcrStatus === 'healthy'" class="text-success">
|
||||
<CheckCircleOutlined /> {{ selectedOcrMessage || '服务正常' }}
|
||||
<CheckCircleFilled /> {{ selectedOcrMessage || '服务正常' }}
|
||||
</span>
|
||||
<span v-else-if="selectedOcrStatus && selectedOcrStatus !== 'unknown'" class="text-warning">
|
||||
⚠️ {{ selectedOcrMessage || '服务异常' }}
|
||||
@ -92,13 +95,13 @@
|
||||
</div>
|
||||
|
||||
<!-- PDF/图片OCR提醒 (Alert样式优化) -->
|
||||
<div v-if="uploadMode === 'file' && hasPdfOrImageFiles && !isOcrEnabled" class="inline-alert warning">
|
||||
<InfoCircleOutlined />
|
||||
<div v-if="hasPdfOrImageFiles && !isOcrEnabled" class="inline-alert warning">
|
||||
<Info :size="16" />
|
||||
<span>检测到PDF或图片文件,建议启用 OCR 以提取文本内容</span>
|
||||
</div>
|
||||
|
||||
<!-- 文件上传区域 -->
|
||||
<div class="upload-area" v-if="uploadMode === 'file'">
|
||||
<div class="upload-area">
|
||||
<a-upload-dragger
|
||||
class="custom-dragger"
|
||||
v-model:fileList="fileList"
|
||||
@ -127,7 +130,7 @@
|
||||
<!-- 同名文件提示 -->
|
||||
<div v-if="sameNameFiles.length > 0" class="conflict-files-panel">
|
||||
<div class="panel-header">
|
||||
<InfoCircleOutlined class="icon-warning" />
|
||||
<Info :size="14" class="icon-warning" />
|
||||
<span>已存在同名文件 ({{ sameNameFiles.length }})</span>
|
||||
</div>
|
||||
<div class="file-list-scroll">
|
||||
@ -138,10 +141,10 @@
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<a-button type="text" size="small" class="action-btn download" @click="downloadSameNameFile(file)">
|
||||
<DownloadOutlined />
|
||||
<Download :size="14" />
|
||||
</a-button>
|
||||
<a-button type="text" size="small" danger class="action-btn delete" @click="deleteSameNameFile(file)">
|
||||
<DeleteOutlined />
|
||||
<Trash2 :size="14" />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,16 +163,18 @@ import { useDatabaseStore } from '@/stores/database';
|
||||
import { ocrApi } from '@/apis/system_api';
|
||||
import { fileApi, documentApi } from '@/apis/knowledge_api';
|
||||
import {
|
||||
FileOutlined,
|
||||
LinkOutlined,
|
||||
SettingOutlined,
|
||||
CheckCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined,
|
||||
CheckCircleFilled,
|
||||
ReloadOutlined,
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
FileUp,
|
||||
FolderUp,
|
||||
RotateCw,
|
||||
CircleHelp,
|
||||
Info,
|
||||
Download,
|
||||
Trash2,
|
||||
} from 'lucide-vue-next';
|
||||
import { h } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
@ -217,6 +222,7 @@ watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
selectedFolderId.value = props.currentFolderId;
|
||||
isFolderUpload.value = props.isFolderMode;
|
||||
uploadMode.value = props.isFolderMode ? 'folder' : 'file';
|
||||
}
|
||||
});
|
||||
|
||||
@ -317,21 +323,26 @@ const uploadModeOptions = computed(() => [
|
||||
{
|
||||
value: 'file',
|
||||
label: h('div', { class: 'segmented-option' }, [
|
||||
h(FileOutlined, { class: 'option-icon' }),
|
||||
h(FileUp, { size: 16, class: 'option-icon' }),
|
||||
h('span', { class: 'option-text' }, '上传文件'),
|
||||
]),
|
||||
},
|
||||
{
|
||||
value: 'url',
|
||||
label: h(Tooltip, { title: 'URL 文档上传与解析功能已禁用,出于安全考虑,当前版本仅支持文件上传' }, {
|
||||
default: () => h('div', { class: 'segmented-option' }, [
|
||||
h(LinkOutlined, { class: 'option-icon' }),
|
||||
h('span', { class: 'option-text' }, '输入网址'),
|
||||
])
|
||||
}),
|
||||
value: 'folder',
|
||||
label: h('div', { class: 'segmented-option' }, [
|
||||
h(FolderUp, { size: 16, class: 'option-icon' }),
|
||||
h('span', { class: 'option-text' }, '上传文件夹'),
|
||||
]),
|
||||
},
|
||||
]);
|
||||
|
||||
watch(uploadMode, (val) => {
|
||||
isFolderUpload.value = val === 'folder';
|
||||
// 切换模式时清空已选文件,避免混淆
|
||||
fileList.value = [];
|
||||
sameNameFiles.value = [];
|
||||
});
|
||||
|
||||
// 文件列表
|
||||
const fileList = ref([]);
|
||||
|
||||
@ -749,51 +760,49 @@ const chunkData = async () => {
|
||||
}
|
||||
|
||||
let success = false;
|
||||
if (uploadMode.value === 'file') {
|
||||
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response?.file_path);
|
||||
// 过滤掉 undefined 或 null 的文件路径
|
||||
const validFiles = files.filter(file => file);
|
||||
if (validFiles.length === 0) {
|
||||
message.error('请先上传文件');
|
||||
return;
|
||||
}
|
||||
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response?.file_path);
|
||||
// 过滤掉 undefined 或 null 的文件路径
|
||||
const validFiles = files.filter(file => file);
|
||||
if (validFiles.length === 0) {
|
||||
message.error('请先上传文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证图片文件是否启用OCR
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'];
|
||||
const hasImageFiles = validFiles.some(filePath => {
|
||||
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
||||
return imageExtensions.includes(ext);
|
||||
// 验证图片文件是否启用OCR
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'];
|
||||
const hasImageFiles = validFiles.some(filePath => {
|
||||
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
||||
return imageExtensions.includes(ext);
|
||||
});
|
||||
|
||||
if (hasImageFiles && chunkParams.value.enable_ocr === 'disable') {
|
||||
message.error({
|
||||
content: '检测到图片文件,必须启用 OCR 才能提取文本内容。请在上方选择 OCR 方式 (RapidOCR/MinerU/MinerU Official/PP-StructureV3) 或移除图片文件。',
|
||||
duration: 5,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
store.state.chunkLoading = true;
|
||||
// 调用 store 的 addFiles 方法
|
||||
await store.addFiles({
|
||||
items: validFiles,
|
||||
contentType: 'file',
|
||||
params: { ...chunkParams.value },
|
||||
parentId: selectedFolderId.value // 传递选中的文件夹 ID
|
||||
});
|
||||
|
||||
if (hasImageFiles && chunkParams.value.enable_ocr === 'disable') {
|
||||
message.error({
|
||||
content: '检测到图片文件,必须启用 OCR 才能提取文本内容。请在上方选择 OCR 方式 (RapidOCR/MinerU/MinerU Official/PP-StructureV3) 或移除图片文件。',
|
||||
duration: 5,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
store.state.chunkLoading = true;
|
||||
// 调用 store 的 addFiles 方法
|
||||
await store.addFiles({
|
||||
items: validFiles,
|
||||
contentType: 'file',
|
||||
params: { ...chunkParams.value },
|
||||
parentId: selectedFolderId.value // 传递选中的文件夹 ID
|
||||
});
|
||||
|
||||
emit('success');
|
||||
handleCancel();
|
||||
fileList.value = [];
|
||||
sameNameFiles.value = [];
|
||||
success = true;
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
message.error('文件上传失败: ' + (error.message || '未知错误'));
|
||||
} finally {
|
||||
store.state.chunkLoading = false;
|
||||
}
|
||||
emit('success');
|
||||
handleCancel();
|
||||
fileList.value = [];
|
||||
sameNameFiles.value = [];
|
||||
success = true;
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
message.error('文件上传失败: ' + (error.message || '未知错误'));
|
||||
} finally {
|
||||
store.state.chunkLoading = false;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
@ -807,6 +816,18 @@ const chunkData = async () => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.footer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.add-files-content {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
@ -838,8 +859,14 @@ const chunkData = async () => {
|
||||
background-color: var(--gray-100);
|
||||
padding: 3px;
|
||||
|
||||
.segmented-option .option-text {
|
||||
margin-left: 6px;
|
||||
.segmented-option {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
.option-text {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -862,14 +889,15 @@ const chunkData = async () => {
|
||||
&.two-cols {
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.col-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0; // Fix flex overflow
|
||||
}
|
||||
|
||||
.col-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0; // Fix flex overflow
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -302,7 +302,7 @@ const formatResultData = (data) => {
|
||||
}
|
||||
|
||||
:deep(.description) {
|
||||
color: var(--gray-600);
|
||||
color: var(--gray-700);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -36,12 +36,12 @@
|
||||
@click="toggleFile(fileGroup.filename)"
|
||||
>
|
||||
<div class="file-info">
|
||||
<FileOutlined />
|
||||
<FileText :size="14" />
|
||||
<span class="file-name">{{ fileGroup.filename }}</span>
|
||||
<span class="chunk-count">{{ fileGroup.chunks.length }} chunks</span>
|
||||
</div>
|
||||
<div class="expand-icon">
|
||||
<DownOutlined :class="{ 'rotated': expandedFiles.has(fileGroup.filename) }" />
|
||||
<ChevronDown :size="14" :class="{ 'rotated': expandedFiles.has(fileGroup.filename) }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
<span v-if="chunk.rerank_score" class="score-item">重排序 {{ (chunk.rerank_score * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<span class="chunk-preview">{{ getPreviewText(chunk.content) }}</span>
|
||||
<EyeOutlined class="view-icon" />
|
||||
<Eye :size="14" class="view-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -108,7 +108,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-meta">
|
||||
<span class="meta-item"><DatabaseOutlined /> ID: {{ selectedChunk.data.metadata.chunk_id || selectedChunk.data.metadata.file_id }}</span>
|
||||
<span class="meta-item"><Database :size="12" /> ID: {{ selectedChunk.data.metadata.chunk_id || selectedChunk.data.metadata.file_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -126,7 +126,7 @@
|
||||
<script setup>
|
||||
import BaseToolCall from '../BaseToolCall.vue';
|
||||
import { ref, computed } from 'vue'
|
||||
import { FileOutlined, DownOutlined, EyeOutlined, DatabaseOutlined } from '@ant-design/icons-vue'
|
||||
import { FileText, ChevronDown, Eye, Database } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
toolCall: {
|
||||
@ -290,7 +290,7 @@ const formatMindmapResult = (content) => {
|
||||
padding: 12px 16px;
|
||||
background: var(--gray-25);
|
||||
font-size: 12px;
|
||||
color: var(--gray-500);
|
||||
color: var(--gray-700);
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
|
||||
@ -332,9 +332,8 @@ const formatMindmapResult = (content) => {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.anticon {
|
||||
color: var(--gray-500);
|
||||
font-size: 13px;
|
||||
svg {
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
@ -350,16 +349,15 @@ const formatMindmapResult = (content) => {
|
||||
|
||||
.chunk-count {
|
||||
font-size: 11px;
|
||||
color: var(--gray-500);
|
||||
color: var(--gray-700);
|
||||
white-space: nowrap;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
color: var(--gray-400);
|
||||
color: var(--gray-700);
|
||||
transition: transform 0.2s ease;
|
||||
font-size: 12px;
|
||||
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
@ -399,7 +397,7 @@ const formatMindmapResult = (content) => {
|
||||
gap: 10px;
|
||||
|
||||
.chunk-index {
|
||||
color: var(--gray-500);
|
||||
color: var(--gray-700);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
min-width: 20px;
|
||||
@ -415,7 +413,7 @@ const formatMindmapResult = (content) => {
|
||||
|
||||
.score-item {
|
||||
font-size: 11px;
|
||||
color: var(--gray-600);
|
||||
color: var(--gray-700);
|
||||
background: var(--gray-25);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
@ -427,7 +425,7 @@ const formatMindmapResult = (content) => {
|
||||
.chunk-preview {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
color: var(--gray-700);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -435,8 +433,7 @@ const formatMindmapResult = (content) => {
|
||||
}
|
||||
|
||||
.view-icon {
|
||||
color: var(--gray-400);
|
||||
font-size: 12px;
|
||||
color: var(--gray-700);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
@ -446,7 +443,7 @@ const formatMindmapResult = (content) => {
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: var(--gray-500);
|
||||
color: var(--gray-700);
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
@ -482,7 +479,7 @@ const formatMindmapResult = (content) => {
|
||||
|
||||
.score-label {
|
||||
font-size: 12px;
|
||||
color: var(--gray-500);
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@ -502,13 +499,13 @@ const formatMindmapResult = (content) => {
|
||||
|
||||
.meta-item {
|
||||
font-size: 11px;
|
||||
color: var(--gray-400);
|
||||
color: var(--gray-700);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.anticon {
|
||||
color: var(--gray-500);
|
||||
svg {
|
||||
color: var(--gray-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user