feat(FileUploadModal): 添加文件夹上传模式并优化图标库

refactor(KnowledgeBaseTool): 替换ant-design图标为lucide-vue-next
style: 调整多个组件的灰色调色板变量
fix(FileTable): 移除分页配置并调整悬浮延迟时间
This commit is contained in:
Wenjie Zhang 2026-01-05 00:46:34 +08:00
parent 88aecfbebc
commit 3812d61566
4 changed files with 140 additions and 136 deletions

View File

@ -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 => {

View File

@ -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
}
}

View File

@ -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;

View File

@ -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);
}
}
}