2024-07-16 18:14:27 +08:00
|
|
|
|
<template>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div class="database-info-container">
|
2024-09-14 02:46:44 +08:00
|
|
|
|
<HeaderComponent
|
2025-07-23 12:54:36 +08:00
|
|
|
|
:title="database.name || '数据库信息加载中'"
|
2025-05-26 20:39:31 +08:00
|
|
|
|
:loading="state.databaseLoading"
|
2025-07-26 03:36:54 +08:00
|
|
|
|
class="database-info-header"
|
2024-09-14 02:46:44 +08:00
|
|
|
|
>
|
2025-06-30 22:29:23 +08:00
|
|
|
|
<template #left>
|
2025-07-26 03:36:54 +08:00
|
|
|
|
<a-button @click="backToDatabase">
|
2025-06-30 22:29:23 +08:00
|
|
|
|
<LeftOutlined />
|
|
|
|
|
|
</a-button>
|
2024-09-14 02:46:44 +08:00
|
|
|
|
</template>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<template #behind-title>
|
|
|
|
|
|
<a-button type="link" @click="showEditModal" :style="{ padding: '0px', color: 'inherit' }">
|
2025-05-10 10:24:18 +08:00
|
|
|
|
<EditOutlined />
|
|
|
|
|
|
</a-button>
|
2024-09-14 02:46:44 +08:00
|
|
|
|
</template>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<template #actions>
|
|
|
|
|
|
<div class="header-info">
|
2025-07-26 15:19:11 +08:00
|
|
|
|
<span class="db-id">ID:
|
|
|
|
|
|
<span style="user-select: all;">{{ database.db_id || 'N/A' }}</span>
|
|
|
|
|
|
</span>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<span class="file-count">{{ database.files ? Object.keys(database.files).length : 0 }} 文件</span>
|
2025-07-23 12:54:36 +08:00
|
|
|
|
<a-tag color="blue">{{ database.embed_info?.name }}</a-tag>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<a-tag
|
|
|
|
|
|
:color="getKbTypeColor(database.kb_type || 'lightrag')"
|
|
|
|
|
|
class="kb-type-tag"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
>
|
|
|
|
|
|
<component :is="getKbTypeIcon(database.kb_type || 'lightrag')" class="type-icon" />
|
|
|
|
|
|
{{ getKbTypeLabel(database.kb_type || 'lightrag') }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2024-09-14 02:46:44 +08:00
|
|
|
|
</HeaderComponent>
|
2025-05-10 10:24:18 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 添加编辑对话框 -->
|
2025-05-26 22:50:47 +08:00
|
|
|
|
<a-modal v-model:open="editModalVisible" title="编辑知识库信息">
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<a-button danger @click="deleteDatabse" style="margin-right: auto; margin-left: 0;">
|
|
|
|
|
|
<DeleteOutlined /> 删除数据库
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button key="back" @click="editModalVisible = false">取消</a-button>
|
2025-06-27 01:55:04 +08:00
|
|
|
|
<a-button key="submit" type="primary" :loading="state.loading" @click="handleEditSubmit">确定</a-button>
|
2025-05-26 22:50:47 +08:00
|
|
|
|
</template>
|
2025-05-10 10:24:18 +08:00
|
|
|
|
<a-form :model="editForm" :rules="rules" ref="editFormRef" layout="vertical">
|
|
|
|
|
|
<a-form-item label="知识库名称" name="name" required>
|
|
|
|
|
|
<a-input v-model:value="editForm.name" placeholder="请输入知识库名称" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="知识库描述" name="description">
|
|
|
|
|
|
<a-textarea v-model:value="editForm.description" placeholder="请输入知识库描述" :rows="4" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
<!-- 分块参数配置弹窗 -->
|
|
|
|
|
|
<a-modal v-model:open="chunkConfigModalVisible" title="分块参数配置" width="500px">
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<a-button key="back" @click="chunkConfigModalVisible = false">取消</a-button>
|
|
|
|
|
|
<a-button key="submit" type="primary" @click="handleChunkConfigSubmit">确定</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="chunk-config-content">
|
|
|
|
|
|
<div class="params-info">
|
|
|
|
|
|
<p>调整分块参数可以控制文本的切分方式,影响检索质量和文档加载效率。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-form
|
|
|
|
|
|
:model="tempChunkParams"
|
|
|
|
|
|
name="chunkConfig"
|
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-form-item label="Chunk Size" name="chunk_size">
|
|
|
|
|
|
<a-input-number v-model:value="tempChunkParams.chunk_size" :min="100" :max="10000" style="width: 100%;" />
|
|
|
|
|
|
<p class="param-description">每个文本片段的最大字符数</p>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="Chunk Overlap" name="chunk_overlap">
|
|
|
|
|
|
<a-input-number v-model:value="tempChunkParams.chunk_overlap" :min="0" :max="1000" style="width: 100%;" />
|
|
|
|
|
|
<p class="param-description">相邻文本片段间的重叠字符数</p>
|
|
|
|
|
|
</a-form-item>
|
2025-07-26 03:36:54 +08:00
|
|
|
|
<a-form-item v-if="isQaSplitSupported" label="QA分割模式" name="use_qa_split">
|
|
|
|
|
|
<a-switch v-model:checked="tempChunkParams.use_qa_split" />
|
|
|
|
|
|
<p class="param-description">启用后将按QA对分割,忽略上述chunk大小设置</p>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item v-if="tempChunkParams.use_qa_split && isQaSplitSupported" label="QA分隔符" name="qa_separator">
|
|
|
|
|
|
<a-input v-model:value="tempChunkParams.qa_separator" placeholder="输入QA分隔符" style="width: 100%;" />
|
|
|
|
|
|
<p class="param-description">用于分割不同QA对的分隔符</p>
|
|
|
|
|
|
</a-form-item>
|
2025-06-18 19:07:29 +08:00
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 添加文件弹窗 -->
|
|
|
|
|
|
<a-modal v-model:open="addFilesModalVisible" title="添加文件" width="800px">
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<a-button key="back" @click="addFilesModalVisible = false">取消</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
key="submit"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="chunkData"
|
|
|
|
|
|
:loading="state.chunkLoading"
|
|
|
|
|
|
:disabled="(uploadMode === 'file' && fileList.length === 0) || (uploadMode === 'url' && !urlList.trim())"
|
|
|
|
|
|
>
|
2025-07-23 19:21:45 +08:00
|
|
|
|
添加到知识库
|
2025-06-18 19:07:29 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="add-files-content">
|
|
|
|
|
|
<div class="upload-header">
|
|
|
|
|
|
<div class="source-selector">
|
|
|
|
|
|
<div class="upload-mode-selector" @click="uploadMode = 'file'" :class="{ active: uploadMode === 'file' }">
|
|
|
|
|
|
<FileOutlined /> 上传文件
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="upload-mode-selector" @click="uploadMode = 'url'" :class="{ active: uploadMode === 'url' }">
|
|
|
|
|
|
<LinkOutlined /> 输入网址
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="config-controls">
|
|
|
|
|
|
<a-button type="dashed" @click="showChunkConfigModal">
|
2025-06-27 01:55:04 +08:00
|
|
|
|
<SettingOutlined /> 分块参数 ({{ chunkParams.chunk_size }}/{{ chunkParams.chunk_overlap }})
|
2025-06-18 19:07:29 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-06-18 21:43:44 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
<div class="ocr-config">
|
|
|
|
|
|
<a-form layout="horizontal">
|
|
|
|
|
|
<a-form-item label="使用OCR" name="enable_ocr">
|
2025-07-21 19:25:07 +08:00
|
|
|
|
<div class="ocr-controls">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="chunkParams.enable_ocr"
|
|
|
|
|
|
:options="enable_ocr_options"
|
|
|
|
|
|
style="width: 220px; margin-right: 12px;"
|
|
|
|
|
|
:disabled="state.ocrHealthChecking"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="dashed"
|
|
|
|
|
|
@click="checkOcrHealth"
|
|
|
|
|
|
:loading="state.ocrHealthChecking"
|
|
|
|
|
|
:icon="h(CheckCircleOutlined)"
|
|
|
|
|
|
>
|
|
|
|
|
|
检查OCR服务
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="param-description">
|
|
|
|
|
|
<div v-if="chunkParams.enable_ocr !== 'disable'" class="ocr-status-info">
|
|
|
|
|
|
<span v-if="getSelectedOcrStatus() && getSelectedOcrStatus() !== 'healthy'" class="ocr-warning">
|
|
|
|
|
|
⚠️ {{ getSelectedOcrMessage() }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-else-if="getSelectedOcrStatus() === 'healthy'" class="ocr-healthy">
|
2025-07-27 00:30:30 +08:00
|
|
|
|
✅ {{ getSelectedOcrMessage() }}
|
2025-07-21 19:25:07 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-07-26 03:36:54 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="qa-split-config" v-if="isQaSplitSupported">
|
|
|
|
|
|
<a-form layout="horizontal">
|
|
|
|
|
|
<a-form-item label="QA分割模式" name="use_qa_split">
|
|
|
|
|
|
<div class="toggle-controls">
|
|
|
|
|
|
<a-switch
|
|
|
|
|
|
v-model:checked="chunkParams.use_qa_split"
|
|
|
|
|
|
style="margin-right: 12px;"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="param-description">
|
|
|
|
|
|
{{ chunkParams.use_qa_split ? '启用QA分割(忽略chunk大小设置)' : '使用普通分割模式' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item
|
|
|
|
|
|
v-if="chunkParams.use_qa_split"
|
|
|
|
|
|
label="QA分隔符"
|
|
|
|
|
|
name="qa_separator"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="chunkParams.qa_separator"
|
|
|
|
|
|
placeholder="输入QA分隔符"
|
|
|
|
|
|
style="width: 200px; margin-right: 12px;"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="param-description">
|
|
|
|
|
|
用于分割不同QA对的分隔符,默认为3个换行符
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</a-form-item>
|
2025-06-18 19:07:29 +08:00
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文件上传区域 -->
|
|
|
|
|
|
<div class="upload" v-if="uploadMode === 'file'">
|
|
|
|
|
|
<a-upload-dragger
|
|
|
|
|
|
class="upload-dragger"
|
|
|
|
|
|
v-model:fileList="fileList"
|
|
|
|
|
|
name="file"
|
|
|
|
|
|
:multiple="true"
|
|
|
|
|
|
:disabled="state.chunkLoading"
|
2025-07-22 17:29:38 +08:00
|
|
|
|
:action="'/api/knowledge/files/upload?db_id=' + databaseId"
|
2025-06-18 19:07:29 +08:00
|
|
|
|
:headers="getAuthHeaders()"
|
|
|
|
|
|
@change="handleFileUpload"
|
|
|
|
|
|
@drop="handleDrop"
|
|
|
|
|
|
>
|
|
|
|
|
|
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
|
|
|
|
|
<p class="ant-upload-hint">
|
|
|
|
|
|
目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</a-upload-dragger>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- URL 输入区域 -->
|
|
|
|
|
|
<div class="url-input" v-else>
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<a-form-item label="网页链接 (每行一个URL)">
|
|
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="urlList"
|
|
|
|
|
|
placeholder="请输入网页链接,每行一个"
|
|
|
|
|
|
:rows="6"
|
|
|
|
|
|
:disabled="state.chunkLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
<p class="url-hint">
|
|
|
|
|
|
支持添加网页内容,系统会自动抓取网页文本并进行分块。请确保URL格式正确且可以公开访问。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<!-- Maximize Graph Modal -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="state.isGraphMaximized"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
width="100%"
|
|
|
|
|
|
wrap-class-name="full-modal"
|
|
|
|
|
|
:mask-closable="false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #title>
|
|
|
|
|
|
<div class="maximized-graph-header">
|
|
|
|
|
|
<h3>知识图谱 (最大化)</h3>
|
|
|
|
|
|
<a-button type="text" @click="toggleGraphMaximize">
|
|
|
|
|
|
<CompressOutlined /> 退出最大化
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="maximized-graph-content">
|
|
|
|
|
|
<div v-if="!isGraphSupported" class="graph-disabled">
|
|
|
|
|
|
<div class="disabled-content">
|
|
|
|
|
|
<h4>知识图谱不可用</h4>
|
|
|
|
|
|
<p>当前知识库类型 "{{ getKbTypeLabel(database.kb_type || 'lightrag') }}" 不支持知识图谱功能。</p>
|
|
|
|
|
|
<p>只有 LightRAG 类型的知识库支持知识图谱。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<KnowledgeGraphViewer
|
|
|
|
|
|
v-else-if="state.isGraphMaximized"
|
|
|
|
|
|
:initial-database-id="databaseId"
|
|
|
|
|
|
:hide-db-selector="true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<!-- 文件详情弹窗 -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="state.fileDetailModalVisible"
|
|
|
|
|
|
:title="selectedFile?.filename || '文件详情'"
|
2025-07-23 19:21:45 +08:00
|
|
|
|
width="1200px"
|
2025-07-23 03:52:22 +08:00
|
|
|
|
:footer="null"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="file-detail-content" v-if="selectedFile">
|
|
|
|
|
|
<div class="file-info-grid">
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label>文件ID:</label>
|
|
|
|
|
|
<span>{{ selectedFile.file_id }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label>上传时间:</label>
|
2025-07-27 13:02:57 +08:00
|
|
|
|
<span>{{ formatStandardTime(Math.round(selectedFile.created_at*1000)) }}</span>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<label>处理状态:</label>
|
|
|
|
|
|
<span class="status-badge" :class="selectedFile.status">
|
2025-07-26 03:36:54 +08:00
|
|
|
|
{{ getStatusText(selectedFile.status) }} - {{ selectedFile.lines.length }} 行
|
2025-07-23 03:52:22 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-05-07 00:38:33 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div class="file-content-section" v-if="selectedFile.lines && selectedFile.lines.length > 0">
|
|
|
|
|
|
<h4>文件内容预览</h4>
|
|
|
|
|
|
<div class="content-lines">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(line, index) in selectedFile.lines"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="content-line"
|
2025-05-26 13:12:17 +08:00
|
|
|
|
>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<span class="line-number">{{ index + 1 }}</span>
|
|
|
|
|
|
<span class="line-text">{{ line.text || line }}</span>
|
|
|
|
|
|
</div>
|
2025-03-23 23:10:32 +08:00
|
|
|
|
</div>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
</div>
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div v-else-if="state.fileDetailLoading" class="loading-container">
|
|
|
|
|
|
<a-spin />
|
|
|
|
|
|
</div>
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div v-else class="empty-content">
|
|
|
|
|
|
<p>暂无文件内容</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div class="unified-layout">
|
|
|
|
|
|
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
|
|
|
|
|
|
<div class="panel-header">
|
2025-07-26 03:36:54 +08:00
|
|
|
|
<div class="search-container">
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="secondary"
|
|
|
|
|
|
@click="showAddFilesModal"
|
|
|
|
|
|
:loading="state.refrashing"
|
|
|
|
|
|
:icon="h(PlusOutlined)"
|
2025-07-26 03:36:54 +08:00
|
|
|
|
>添加文件</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel-actions">
|
2025-07-27 02:56:13 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
@click="handleRefresh"
|
|
|
|
|
|
:loading="state.refrashing"
|
|
|
|
|
|
:icon="h(ReloadOutlined)"
|
|
|
|
|
|
title="刷新"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
@click="toggleAutoRefresh"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:type="state.autoRefresh ? 'primary' : 'default'"
|
|
|
|
|
|
title="自动刷新文件状态"
|
|
|
|
|
|
class="auto-refresh-btn panel-action-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
Auto
|
|
|
|
|
|
</a-button>
|
2025-07-26 03:36:54 +08:00
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="filenameFilter"
|
|
|
|
|
|
placeholder="搜索文件名"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style="width: 120px; margin-right: 8px; border-radius: 6px; padding: 4px 8px;"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
@change="onFilterChange"
|
|
|
|
|
|
/>
|
2025-07-26 15:19:11 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
@click="toggleRightPanel"
|
|
|
|
|
|
:icon="h(ChevronLast)"
|
|
|
|
|
|
title="切换右侧面板"
|
2025-07-27 02:56:13 +08:00
|
|
|
|
class="panel-action-btn expanded"
|
2025-07-26 15:19:11 +08:00
|
|
|
|
:class="{ 'active': state.rightPanelVisible }"
|
|
|
|
|
|
/>
|
2024-09-03 16:37:59 +08:00
|
|
|
|
</div>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
</div>
|
2025-06-30 22:29:23 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div class="batch-actions-compact" v-if="selectedRowKeys.length > 0">
|
2025-07-27 00:30:30 +08:00
|
|
|
|
<div class="batch-info">
|
|
|
|
|
|
<span>{{ selectedRowKeys.length }} 项</span>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="selectAllFailedFiles"
|
|
|
|
|
|
:disabled="state.lock"
|
|
|
|
|
|
title="选择所有失败的文件"
|
|
|
|
|
|
>
|
|
|
|
|
|
选择失败
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
danger
|
|
|
|
|
|
@click="handleBatchDelete"
|
|
|
|
|
|
:loading="state.batchDeleting"
|
|
|
|
|
|
:disabled="!canBatchDelete"
|
|
|
|
|
|
:icon="h(DeleteOutlined)"
|
|
|
|
|
|
title="批量删除"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:columns="columnsCompact"
|
2025-07-26 03:36:54 +08:00
|
|
|
|
:data-source="filteredFiles"
|
2025-07-23 03:52:22 +08:00
|
|
|
|
row-key="file_id"
|
|
|
|
|
|
class="my-table-compact"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:pagination="paginationCompact"
|
|
|
|
|
|
:row-selection="{
|
|
|
|
|
|
selectedRowKeys: selectedRowKeys,
|
|
|
|
|
|
onChange: onSelectChange,
|
|
|
|
|
|
getCheckboxProps: getCheckboxProps
|
2025-07-26 03:36:54 +08:00
|
|
|
|
}"
|
|
|
|
|
|
:locale="{
|
|
|
|
|
|
emptyText: emptyText
|
2025-07-26 15:19:11 +08:00
|
|
|
|
}"
|
|
|
|
|
|
@change="handleTableChange">
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<template #bodyCell="{ column, text, record }">
|
2025-07-26 11:18:54 +08:00
|
|
|
|
<a-button v-if="column.key === 'filename'" class="main-btn" type="link" @click="openFileDetail(record)">
|
|
|
|
|
|
<component :is="getFileIcon(text)" :style="{ marginRight: '6px', color: getFileIconColor(text) }" />
|
|
|
|
|
|
{{ text }}
|
|
|
|
|
|
</a-button>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<span v-else-if="column.key === 'type'" :class="['span-type', text]">{{ text?.toUpperCase() }}</span>
|
2025-07-26 15:19:11 +08:00
|
|
|
|
<div v-else-if="column.key === 'status'" style="display: flex; align-items: center; justify-content: flex-end;">
|
|
|
|
|
|
<CheckCircleFilled v-if="text === 'done'" style="color: #41A317;"/>
|
|
|
|
|
|
<CloseCircleFilled v-else-if="text === 'failed'" style="color: #FF4D4F;"/>
|
|
|
|
|
|
<HourglassFilled v-else-if="text === 'processing'" style="color: #1677FF;"/>
|
|
|
|
|
|
<ClockCircleFilled v-else-if="text === 'waiting'" style="color: #FFCD43;"/>
|
|
|
|
|
|
</div>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-07-26 15:19:11 +08:00
|
|
|
|
<a-tooltip v-else-if="column.key === 'created_at'" :title="formatRelativeTime(Math.round(text*1000))" placement="left">
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<span>{{ formatRelativeTime(Math.round(text*1000)) }}</span>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="column.key === 'action'" style="display: flex; gap: 4px;">
|
|
|
|
|
|
<a-button class="del-btn" type="text"
|
|
|
|
|
|
@click="handleDeleteFile(record.file_id)"
|
|
|
|
|
|
:disabled="state.lock || record.status === 'processing' || record.status === 'waiting'"
|
|
|
|
|
|
:icon="h(DeleteOutlined)"
|
|
|
|
|
|
title="删除"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span v-else>{{ text }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="resize-handle" ref="resizeHandle"></div>
|
|
|
|
|
|
|
2025-07-26 15:19:11 +08:00
|
|
|
|
<div class="right-panel" :style="{ width: (100 - leftPanelWidth) + '%', display: state.rightPanelVisible ? 'flex' : 'none' }">
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
<div class="graph-section" :class="{ collapsed: !panels.graph.visible }" :style="computePanelStyles().graph" v-if="isGraphSupported" >
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<h3>知识图谱</h3>
|
|
|
|
|
|
<div v-if="graphStats.displayed_nodes > 0 || graphStats.displayed_edges > 0" class="graph-stats">
|
|
|
|
|
|
<a-tag color="blue" size="small">节点: {{ graphStats.displayed_nodes }}</a-tag>
|
|
|
|
|
|
<a-tag color="green" size="small">边: {{ graphStats.displayed_edges }}</a-tag>
|
|
|
|
|
|
<a-tag v-if="graphStats.is_truncated" color="red" size="small">已截断</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel-actions">
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
2025-07-21 18:18:47 +08:00
|
|
|
|
size="small"
|
2025-07-23 03:52:22 +08:00
|
|
|
|
:icon="h(ReloadOutlined)"
|
|
|
|
|
|
title="刷新视图"
|
|
|
|
|
|
@click="refreshGraph"
|
|
|
|
|
|
:disabled="!isGraphSupported"
|
2025-07-21 18:18:47 +08:00
|
|
|
|
>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
刷新视图
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:icon="h(DeleteOutlined)"
|
|
|
|
|
|
title="清空视图"
|
|
|
|
|
|
@click="clearGraph"
|
|
|
|
|
|
:disabled="!isGraphSupported"
|
2025-07-21 18:18:47 +08:00
|
|
|
|
>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
清空视图
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:icon="h(ExpandOutlined)"
|
|
|
|
|
|
title="最大化"
|
|
|
|
|
|
@click="toggleGraphMaximize"
|
|
|
|
|
|
:disabled="!isGraphSupported"
|
|
|
|
|
|
>
|
|
|
|
|
|
最大化
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="togglePanel('graph')"
|
|
|
|
|
|
title="折叠/展开"
|
|
|
|
|
|
>
|
|
|
|
|
|
<component :is="panels.graph.visible ? UpOutlined : DownOutlined" />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="graph-container-compact content" v-show="panels.graph.visible">
|
|
|
|
|
|
<div v-if="!isGraphSupported" class="graph-disabled">
|
|
|
|
|
|
<div class="disabled-content">
|
|
|
|
|
|
<h4>知识图谱不可用</h4>
|
|
|
|
|
|
<p>当前知识库类型 "{{ getKbTypeLabel(database.kb_type || 'lightrag') }}" 不支持知识图谱功能。</p>
|
|
|
|
|
|
<p>只有 LightRAG 类型的知识库支持知识图谱。</p>
|
|
|
|
|
|
</div>
|
2025-07-21 18:18:47 +08:00
|
|
|
|
</div>
|
2025-07-18 11:29:12 +08:00
|
|
|
|
<KnowledgeGraphViewer
|
2025-07-21 18:18:47 +08:00
|
|
|
|
v-else
|
2025-07-18 11:29:12 +08:00
|
|
|
|
:initial-database-id="databaseId"
|
2025-06-30 22:29:23 +08:00
|
|
|
|
:hide-db-selector="true"
|
2025-07-23 03:52:22 +08:00
|
|
|
|
:hide-stats="true"
|
|
|
|
|
|
@update:stats="handleStatsUpdate"
|
|
|
|
|
|
ref="graphViewerRef"
|
2025-06-30 22:29:23 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="resize-handle-horizontal" ref="resizeHandleHorizontal" v-show="panels.query.visible && panels.graph.visible"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-section" :class="{ collapsed: !panels.query.visible }" :style="computePanelStyles().query">
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<h3>检索测试</h3>
|
|
|
|
|
|
<div class="panel-actions">
|
|
|
|
|
|
<a-popover trigger="click" placement="bottomRight" class="query-params-popover">
|
|
|
|
|
|
<template #content>
|
|
|
|
|
|
<div class="query-params-compact">
|
|
|
|
|
|
<div v-if="state.queryParamsLoading" class="params-loading">
|
|
|
|
|
|
<a-spin size="small" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="params-grid">
|
|
|
|
|
|
<div v-for="param in queryParams" :key="param.key" class="param-item">
|
|
|
|
|
|
<label>{{ param.label }}:</label>
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-if="param.type === 'select'"
|
|
|
|
|
|
v-model:value="meta[param.key]"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style="width: 80px;"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-select-option
|
|
|
|
|
|
v-for="option in param.options"
|
|
|
|
|
|
:key="option.value"
|
|
|
|
|
|
:value="option.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
<a-switch
|
|
|
|
|
|
v-else-if="param.type === 'boolean'"
|
|
|
|
|
|
v-model:checked="meta[param.key]"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-else-if="param.type === 'number'"
|
|
|
|
|
|
v-model:value="meta[param.key]"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style="width: 60px;"
|
|
|
|
|
|
:min="param.min || 0"
|
|
|
|
|
|
:max="param.max || 100"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:icon="h(SettingOutlined)"
|
|
|
|
|
|
title="查询参数"
|
|
|
|
|
|
>查询参数</a-button>
|
|
|
|
|
|
</a-popover>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="togglePanel('query')"
|
|
|
|
|
|
title="折叠/展开"
|
|
|
|
|
|
>
|
|
|
|
|
|
<component :is="panels.query.visible ? UpOutlined : DownOutlined" />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-content content" v-show="panels.query.visible">
|
|
|
|
|
|
<div class="query-input-row">
|
2025-07-27 02:56:13 +08:00
|
|
|
|
<a-textarea
|
2025-07-23 03:52:22 +08:00
|
|
|
|
v-model:value="queryText"
|
|
|
|
|
|
placeholder="输入查询内容"
|
2025-07-27 02:56:13 +08:00
|
|
|
|
:auto-size="{ minRows: 3, maxRows: 6 }"
|
|
|
|
|
|
class="compact-query-textarea"
|
2025-07-23 03:52:22 +08:00
|
|
|
|
/>
|
2025-07-27 02:56:13 +08:00
|
|
|
|
<div class="query-actions">
|
|
|
|
|
|
<div class="search-row">
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
@click="onQuery"
|
|
|
|
|
|
:loading="state.searchLoading"
|
|
|
|
|
|
class="search-button"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #icon>
|
|
|
|
|
|
<SearchOutlined />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
搜索
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<div class="query-examples-compact">
|
|
|
|
|
|
<span class="examples-label">示例:</span>
|
|
|
|
|
|
<div class="examples-container">
|
|
|
|
|
|
<transition name="fade" mode="out-in">
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
:key="currentExampleIndex"
|
|
|
|
|
|
@click="useQueryExample(queryExamples[currentExampleIndex])"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="example-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ queryExamples[currentExampleIndex] }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
</div>
|
2025-06-30 22:29:23 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<div class="query-results" v-if="queryResult">
|
|
|
|
|
|
{{ queryResult }}
|
|
|
|
|
|
</div>
|
2025-06-27 01:55:04 +08:00
|
|
|
|
</div>
|
2025-07-23 03:52:22 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2024-07-16 18:14:27 +08:00
|
|
|
|
</div>
|
2024-09-14 02:46:44 +08:00
|
|
|
|
</div>
|
2024-07-16 18:14:27 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-06-30 22:29:23 +08:00
|
|
|
|
import { onMounted, reactive, ref, watch, toRaw, onUnmounted, computed, h } from 'vue';
|
2024-07-17 18:52:20 +08:00
|
|
|
|
import { message, Modal } from 'ant-design-vue';
|
2024-07-16 18:14:27 +08:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router';
|
2024-09-09 17:07:03 +08:00
|
|
|
|
import { useConfigStore } from '@/stores/config'
|
2025-05-09 16:11:03 +08:00
|
|
|
|
import { useUserStore } from '@/stores/user'
|
2025-07-22 17:29:38 +08:00
|
|
|
|
import { databaseApi, documentApi, queryApi, fileApi } from '@/apis/knowledge_api'
|
|
|
|
|
|
import { ocrApi } from '@/apis/system_api'
|
2024-07-16 18:14:27 +08:00
|
|
|
|
import {
|
2025-03-23 23:10:32 +08:00
|
|
|
|
ReadOutlined,
|
2024-07-16 18:14:27 +08:00
|
|
|
|
LeftOutlined,
|
2025-07-23 03:52:22 +08:00
|
|
|
|
RightOutlined,
|
2024-07-16 18:14:27 +08:00
|
|
|
|
CheckCircleFilled,
|
2025-07-21 19:25:07 +08:00
|
|
|
|
CheckCircleOutlined,
|
2024-07-16 18:14:27 +08:00
|
|
|
|
HourglassFilled,
|
|
|
|
|
|
CloseCircleFilled,
|
|
|
|
|
|
ClockCircleFilled,
|
2024-07-17 18:52:20 +08:00
|
|
|
|
DeleteOutlined,
|
2024-07-29 01:00:02 +08:00
|
|
|
|
SearchOutlined,
|
2025-03-23 23:10:32 +08:00
|
|
|
|
LoadingOutlined,
|
2025-05-07 00:38:33 +08:00
|
|
|
|
FileOutlined,
|
|
|
|
|
|
LinkOutlined,
|
2025-05-10 10:24:18 +08:00
|
|
|
|
EditOutlined,
|
2025-05-26 13:12:17 +08:00
|
|
|
|
PlusOutlined,
|
2025-06-18 19:07:29 +08:00
|
|
|
|
SettingOutlined,
|
2025-07-21 18:18:47 +08:00
|
|
|
|
DatabaseOutlined,
|
|
|
|
|
|
ThunderboltOutlined,
|
2025-07-23 03:52:22 +08:00
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
UpOutlined,
|
|
|
|
|
|
DownOutlined,
|
|
|
|
|
|
ExpandOutlined,
|
|
|
|
|
|
CompressOutlined,
|
2025-07-26 11:18:54 +08:00
|
|
|
|
FileTextFilled,
|
|
|
|
|
|
FileImageFilled,
|
|
|
|
|
|
FileMarkdownFilled,
|
|
|
|
|
|
FilePdfFilled,
|
|
|
|
|
|
FileWordFilled,
|
|
|
|
|
|
FileExcelFilled,
|
|
|
|
|
|
FileUnknownFilled,
|
2024-07-16 18:14:27 +08:00
|
|
|
|
} from '@ant-design/icons-vue'
|
2025-06-30 22:29:23 +08:00
|
|
|
|
import HeaderComponent from '@/components/HeaderComponent.vue';
|
|
|
|
|
|
import KnowledgeGraphViewer from '@/components/KnowledgeGraphViewer.vue';
|
2025-07-26 15:19:11 +08:00
|
|
|
|
import { Waypoints, Database, Zap, ChevronLast } from 'lucide-vue-next';
|
2025-06-30 22:29:23 +08:00
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const databaseId = ref(route.params.database_id);
|
|
|
|
|
|
const database = ref({});
|
|
|
|
|
|
|
|
|
|
|
|
const fileList = ref([]);
|
|
|
|
|
|
const selectedFile = ref(null);
|
|
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
// 文件名过滤
|
|
|
|
|
|
const filenameFilter = ref('');
|
|
|
|
|
|
|
2024-07-29 01:00:02 +08:00
|
|
|
|
// 查询测试
|
|
|
|
|
|
const queryText = ref('');
|
|
|
|
|
|
const queryResult = ref(null)
|
2024-09-09 17:07:03 +08:00
|
|
|
|
const configStore = useConfigStore()
|
2024-07-29 01:00:02 +08:00
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
const state = reactive({
|
2025-05-26 20:39:31 +08:00
|
|
|
|
databaseLoading: false,
|
2025-03-23 23:10:32 +08:00
|
|
|
|
adding: false,
|
2024-07-16 18:14:27 +08:00
|
|
|
|
refrashing: false,
|
2024-07-29 01:00:02 +08:00
|
|
|
|
searchLoading: false,
|
2024-07-16 18:14:27 +08:00
|
|
|
|
lock: false,
|
2025-05-26 13:59:05 +08:00
|
|
|
|
fileDetailModalVisible: false,
|
|
|
|
|
|
fileDetailLoading: false,
|
2024-07-17 18:52:20 +08:00
|
|
|
|
refreshInterval: null,
|
2025-03-23 23:10:32 +08:00
|
|
|
|
curPage: "files",
|
2025-05-26 20:39:31 +08:00
|
|
|
|
batchDeleting: false,
|
2025-07-27 00:30:30 +08:00
|
|
|
|
batchDeleteProgress: { current: 0, total: 0 }, // 批量删除进度
|
2025-05-26 20:39:31 +08:00
|
|
|
|
chunkLoading: false,
|
2025-06-27 01:55:04 +08:00
|
|
|
|
autoRefresh: false,
|
|
|
|
|
|
loading: false,
|
2025-07-21 18:18:47 +08:00
|
|
|
|
queryParamsLoading: false,
|
2025-07-21 19:25:07 +08:00
|
|
|
|
ocrHealthChecking: false,
|
2025-07-23 03:52:22 +08:00
|
|
|
|
isGraphMaximized: false,
|
2025-07-26 15:19:11 +08:00
|
|
|
|
rightPanelVisible: true, // 添加这一行
|
2025-07-23 03:52:22 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性:是否支持知识图谱
|
|
|
|
|
|
const isGraphSupported = computed(() => {
|
|
|
|
|
|
const kbType = database.value.kb_type?.toLowerCase();
|
|
|
|
|
|
return kbType === 'lightrag';
|
2025-07-21 19:25:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
// 计算属性:是否支持QA分割
|
|
|
|
|
|
const isQaSplitSupported = computed(() => {
|
|
|
|
|
|
const kbType = database.value.kb_type?.toLowerCase();
|
|
|
|
|
|
return kbType === 'chroma' || kbType === 'milvus';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// 切换图谱最大化状态
|
|
|
|
|
|
const toggleGraphMaximize = () => {
|
|
|
|
|
|
state.isGraphMaximized = !state.isGraphMaximized;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 图谱相关数据
|
|
|
|
|
|
const graphViewerRef = ref(null);
|
|
|
|
|
|
const graphStats = ref({
|
|
|
|
|
|
displayed_nodes: 0,
|
|
|
|
|
|
displayed_edges: 0,
|
|
|
|
|
|
is_truncated: false
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 处理图谱统计信息更新
|
|
|
|
|
|
const handleStatsUpdate = (stats) => {
|
|
|
|
|
|
graphStats.value = stats;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-21 19:25:07 +08:00
|
|
|
|
// OCR服务健康状态
|
|
|
|
|
|
const ocrHealthStatus = ref({
|
|
|
|
|
|
rapid_ocr: { status: 'unknown', message: '' },
|
|
|
|
|
|
mineru_ocr: { status: 'unknown', message: '' },
|
|
|
|
|
|
paddlex_ocr: { status: 'unknown', message: '' }
|
2024-07-16 18:14:27 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-21 18:18:47 +08:00
|
|
|
|
// 动态查询参数
|
|
|
|
|
|
const queryParams = ref([])
|
|
|
|
|
|
const meta = reactive({});
|
2024-07-29 01:00:02 +08:00
|
|
|
|
|
2025-07-21 19:25:07 +08:00
|
|
|
|
// OCR健康检查函数
|
|
|
|
|
|
const checkOcrHealth = async () => {
|
|
|
|
|
|
if (state.ocrHealthChecking) return;
|
|
|
|
|
|
|
|
|
|
|
|
state.ocrHealthChecking = true;
|
|
|
|
|
|
try {
|
2025-07-22 17:29:38 +08:00
|
|
|
|
const healthData = await ocrApi.getHealth();
|
2025-07-21 19:25:07 +08:00
|
|
|
|
ocrHealthStatus.value = healthData.services;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('OCR健康检查失败:', error);
|
|
|
|
|
|
message.error('OCR服务健康检查失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
state.ocrHealthChecking = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 生成OCR选项的计算属性,包含健康状态信息
|
|
|
|
|
|
const enable_ocr_options = computed(() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'disable',
|
|
|
|
|
|
label: '不启用',
|
|
|
|
|
|
title: '不启用'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'onnx_rapid_ocr',
|
|
|
|
|
|
label: getRapidOcrLabel(),
|
|
|
|
|
|
title: 'ONNX with RapidOCR',
|
|
|
|
|
|
disabled: ocrHealthStatus.value.rapid_ocr.status === 'unavailable' || ocrHealthStatus.value.rapid_ocr.status === 'error'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'mineru_ocr',
|
|
|
|
|
|
label: getMinerULabel(),
|
|
|
|
|
|
title: 'MinerU OCR',
|
|
|
|
|
|
disabled: ocrHealthStatus.value.mineru_ocr.status === 'unavailable' || ocrHealthStatus.value.mineru_ocr.status === 'error'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: 'paddlex_ocr',
|
|
|
|
|
|
label: getPaddleXLabel(),
|
|
|
|
|
|
title: 'PaddleX OCR',
|
|
|
|
|
|
disabled: ocrHealthStatus.value.paddlex_ocr.status === 'unavailable' || ocrHealthStatus.value.paddlex_ocr.status === 'error'
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// OCR选项标签生成函数
|
|
|
|
|
|
const getRapidOcrLabel = () => {
|
|
|
|
|
|
const status = ocrHealthStatus.value.rapid_ocr.status;
|
|
|
|
|
|
const statusIcons = {
|
|
|
|
|
|
'healthy': '✅',
|
|
|
|
|
|
'unavailable': '❌',
|
|
|
|
|
|
'error': '⚠️',
|
|
|
|
|
|
'unknown': '❓'
|
|
|
|
|
|
};
|
|
|
|
|
|
return `${statusIcons[status] || '❓'} RapidOCR (ONNX)`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getMinerULabel = () => {
|
|
|
|
|
|
const status = ocrHealthStatus.value.mineru_ocr.status;
|
|
|
|
|
|
const statusIcons = {
|
|
|
|
|
|
'healthy': '✅',
|
|
|
|
|
|
'unavailable': '❌',
|
|
|
|
|
|
'unhealthy': '⚠️',
|
|
|
|
|
|
'timeout': '⏰',
|
|
|
|
|
|
'error': '⚠️',
|
|
|
|
|
|
'unknown': '❓'
|
|
|
|
|
|
};
|
|
|
|
|
|
return `${statusIcons[status] || '❓'} MinerU OCR`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getPaddleXLabel = () => {
|
|
|
|
|
|
const status = ocrHealthStatus.value.paddlex_ocr.status;
|
|
|
|
|
|
const statusIcons = {
|
|
|
|
|
|
'healthy': '✅',
|
|
|
|
|
|
'unavailable': '❌',
|
|
|
|
|
|
'unhealthy': '⚠️',
|
|
|
|
|
|
'timeout': '⏰',
|
|
|
|
|
|
'error': '⚠️',
|
|
|
|
|
|
'unknown': '❓'
|
|
|
|
|
|
};
|
|
|
|
|
|
return `${statusIcons[status] || '❓'} PaddleX OCR`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前选中OCR服务的状态
|
|
|
|
|
|
const getSelectedOcrStatus = () => {
|
|
|
|
|
|
switch (chunkParams.value.enable_ocr) {
|
|
|
|
|
|
case 'onnx_rapid_ocr':
|
|
|
|
|
|
return ocrHealthStatus.value.rapid_ocr.status;
|
|
|
|
|
|
case 'mineru_ocr':
|
|
|
|
|
|
return ocrHealthStatus.value.mineru_ocr.status;
|
|
|
|
|
|
case 'paddlex_ocr':
|
|
|
|
|
|
return ocrHealthStatus.value.paddlex_ocr.status;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前选中OCR服务的状态消息
|
|
|
|
|
|
const getSelectedOcrMessage = () => {
|
|
|
|
|
|
switch (chunkParams.value.enable_ocr) {
|
|
|
|
|
|
case 'onnx_rapid_ocr':
|
|
|
|
|
|
return ocrHealthStatus.value.rapid_ocr.message;
|
|
|
|
|
|
case 'mineru_ocr':
|
|
|
|
|
|
return ocrHealthStatus.value.mineru_ocr.message;
|
|
|
|
|
|
case 'paddlex_ocr':
|
|
|
|
|
|
return ocrHealthStatus.value.paddlex_ocr.message;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 验证OCR服务可用性
|
|
|
|
|
|
const validateOcrService = () => {
|
|
|
|
|
|
if (chunkParams.value.enable_ocr === 'disable') {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const status = getSelectedOcrStatus();
|
|
|
|
|
|
if (status === 'unavailable' || status === 'error') {
|
|
|
|
|
|
const ocrMessage = getSelectedOcrMessage();
|
|
|
|
|
|
message.error(`OCR服务不可用: ${ocrMessage}`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
2025-05-23 15:30:14 +08:00
|
|
|
|
|
2025-07-21 18:18:47 +08:00
|
|
|
|
// 加载知识库类型特定的查询参数
|
|
|
|
|
|
const loadQueryParams = async () => {
|
|
|
|
|
|
if (!databaseId.value) return
|
|
|
|
|
|
|
|
|
|
|
|
state.queryParamsLoading = true
|
|
|
|
|
|
try {
|
2025-07-22 17:29:38 +08:00
|
|
|
|
const response = await queryApi.getKnowledgeBaseQueryParams(databaseId.value)
|
2025-07-21 18:18:47 +08:00
|
|
|
|
queryParams.value = response.params?.options || []
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化meta对象的默认值
|
|
|
|
|
|
queryParams.value.forEach(param => {
|
|
|
|
|
|
if (!(param.key in meta)) {
|
|
|
|
|
|
meta[param.key] = param.default
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Loaded query params:', queryParams.value)
|
|
|
|
|
|
console.log('Initialized meta:', meta)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to load query params:', error)
|
|
|
|
|
|
message.error('加载查询参数失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
state.queryParamsLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-05-26 13:59:05 +08:00
|
|
|
|
const pagination = ref({
|
2025-05-26 20:39:31 +08:00
|
|
|
|
pageSize: 15,
|
2025-05-26 13:59:05 +08:00
|
|
|
|
current: 1,
|
2025-05-26 20:39:31 +08:00
|
|
|
|
total: computed(() => database.value?.files?.length || 0),
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
onChange: (page, pageSize) => pagination.value.current = page,
|
2025-05-26 13:59:05 +08:00
|
|
|
|
showTotal: (total, range) => `共 ${total} 条`,
|
2025-05-26 20:39:31 +08:00
|
|
|
|
onShowSizeChange: (current, pageSize) => pagination.value.pageSize = pageSize,
|
2025-05-26 13:59:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2024-07-29 01:00:02 +08:00
|
|
|
|
const onQuery = () => {
|
|
|
|
|
|
console.log(queryText.value)
|
|
|
|
|
|
state.searchLoading = true
|
|
|
|
|
|
if (!queryText.value.trim()) {
|
|
|
|
|
|
message.error('请输入查询内容')
|
|
|
|
|
|
state.searchLoading = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// Remove db_id from meta to avoid duplicate parameter error
|
|
|
|
|
|
const queryMeta = { ...toRaw(meta) }
|
|
|
|
|
|
delete queryMeta.db_id
|
2025-05-05 16:31:35 +08:00
|
|
|
|
|
2025-05-04 21:04:01 +08:00
|
|
|
|
try {
|
2025-07-23 03:52:22 +08:00
|
|
|
|
queryApi.queryTest(database.value.db_id, queryText.value.trim(), queryMeta)
|
2025-05-04 21:04:01 +08:00
|
|
|
|
.then(data => {
|
|
|
|
|
|
console.log(data)
|
|
|
|
|
|
queryResult.value = data
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
message.error(error.message)
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
state.searchLoading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
2024-07-29 01:00:02 +08:00
|
|
|
|
console.error(error)
|
|
|
|
|
|
message.error(error.message)
|
|
|
|
|
|
state.searchLoading = false
|
2025-05-04 21:04:01 +08:00
|
|
|
|
}
|
2024-07-29 01:00:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
const handleFileUpload = (event) => {
|
|
|
|
|
|
console.log(event)
|
|
|
|
|
|
console.log(fileList.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDrop = (event) => {
|
|
|
|
|
|
console.log(event)
|
|
|
|
|
|
console.log(fileList.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const afterOpenChange = (visible) => {
|
|
|
|
|
|
if (!visible) {
|
|
|
|
|
|
selectedFile.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const backToDatabase = () => {
|
|
|
|
|
|
router.push('/database')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleRefresh = () => {
|
|
|
|
|
|
state.refrashing = true
|
|
|
|
|
|
getDatabaseInfo().then(() => {
|
|
|
|
|
|
state.refrashing = false
|
|
|
|
|
|
console.log(database.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
|
2025-05-26 13:12:17 +08:00
|
|
|
|
|
2024-07-17 18:52:20 +08:00
|
|
|
|
const deleteDatabse = () => {
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '删除数据库',
|
|
|
|
|
|
content: '确定要删除该数据库吗?',
|
|
|
|
|
|
okText: '确认',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: () => {
|
|
|
|
|
|
state.lock = true
|
2025-07-22 17:29:38 +08:00
|
|
|
|
databaseApi.deleteDatabase(databaseId.value)
|
2025-05-02 23:56:59 +08:00
|
|
|
|
.then(data => {
|
|
|
|
|
|
console.log(data)
|
|
|
|
|
|
message.success(data.message || '删除成功')
|
|
|
|
|
|
router.push('/database')
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
message.error(error.message || '删除失败')
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
state.lock = false
|
|
|
|
|
|
})
|
2024-07-17 18:52:20 +08:00
|
|
|
|
},
|
|
|
|
|
|
onCancel: () => {
|
|
|
|
|
|
console.log('Cancel');
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
const openFileDetail = (record) => {
|
2025-05-26 13:59:05 +08:00
|
|
|
|
// 先打开弹窗
|
2025-06-27 01:55:04 +08:00
|
|
|
|
if (record.status !== 'done') {
|
|
|
|
|
|
message.error('文件未处理完成,请稍后再试');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-05-26 13:59:05 +08:00
|
|
|
|
state.fileDetailModalVisible = true;
|
|
|
|
|
|
selectedFile.value = {
|
|
|
|
|
|
...record,
|
|
|
|
|
|
lines: []
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 设置加载状态
|
|
|
|
|
|
state.fileDetailLoading = true;
|
|
|
|
|
|
state.lock = true;
|
2025-05-05 16:31:35 +08:00
|
|
|
|
|
2025-05-04 21:04:01 +08:00
|
|
|
|
try {
|
2025-07-22 17:29:38 +08:00
|
|
|
|
documentApi.getDocumentInfo(databaseId.value, record.file_id)
|
2025-05-04 21:04:01 +08:00
|
|
|
|
.then(data => {
|
2025-05-26 13:59:05 +08:00
|
|
|
|
console.log(data);
|
2025-05-04 21:04:01 +08:00
|
|
|
|
if (data.status == "failed") {
|
2025-05-26 13:59:05 +08:00
|
|
|
|
message.error(data.message);
|
|
|
|
|
|
state.fileDetailModalVisible = false;
|
|
|
|
|
|
return;
|
2025-05-04 21:04:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
selectedFile.value = {
|
|
|
|
|
|
...record,
|
|
|
|
|
|
lines: data.lines || []
|
2025-05-26 13:59:05 +08:00
|
|
|
|
};
|
2025-05-04 21:04:01 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
2025-05-26 13:59:05 +08:00
|
|
|
|
console.error(error);
|
|
|
|
|
|
message.error(error.message);
|
|
|
|
|
|
state.fileDetailModalVisible = false;
|
2025-05-04 21:04:01 +08:00
|
|
|
|
})
|
2025-05-26 13:59:05 +08:00
|
|
|
|
.finally(() => {
|
|
|
|
|
|
state.fileDetailLoading = false;
|
|
|
|
|
|
state.lock = false;
|
|
|
|
|
|
});
|
2025-05-04 21:04:01 +08:00
|
|
|
|
} catch (error) {
|
2025-05-26 13:59:05 +08:00
|
|
|
|
console.error(error);
|
2025-05-26 22:50:47 +08:00
|
|
|
|
message.error('获取文件详情失败!!!!');
|
2025-05-26 13:59:05 +08:00
|
|
|
|
state.fileDetailLoading = false;
|
|
|
|
|
|
state.lock = false;
|
|
|
|
|
|
state.fileDetailModalVisible = false;
|
2025-05-04 21:04:01 +08:00
|
|
|
|
}
|
2024-07-16 18:14:27 +08:00
|
|
|
|
}
|
2025-05-26 13:59:05 +08:00
|
|
|
|
|
2025-07-27 13:02:57 +08:00
|
|
|
|
// Format relative time with more granularity: days ago, weeks ago, months ago
|
2025-06-18 21:43:44 +08:00
|
|
|
|
const formatRelativeTime = (timestamp, offset = 0) => {
|
2025-07-27 13:02:57 +08:00
|
|
|
|
// If you want to adjust to UTC+8, set offset to 8, otherwise 0
|
|
|
|
|
|
const timezoneOffset = offset * 60 * 60 * 1000; // offset in milliseconds
|
2025-05-15 23:06:19 +08:00
|
|
|
|
const adjustedTimestamp = timestamp + timezoneOffset;
|
|
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
const now = Date.now();
|
2025-05-15 23:06:19 +08:00
|
|
|
|
const secondsPast = (now - adjustedTimestamp) / 1000;
|
|
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
if (secondsPast < 60) {
|
|
|
|
|
|
return Math.round(secondsPast) + ' 秒前';
|
|
|
|
|
|
} else if (secondsPast < 3600) {
|
|
|
|
|
|
return Math.round(secondsPast / 60) + ' 分钟前';
|
|
|
|
|
|
} else if (secondsPast < 86400) {
|
|
|
|
|
|
return Math.round(secondsPast / 3600) + ' 小时前';
|
2025-07-27 13:02:57 +08:00
|
|
|
|
} else if (secondsPast < 86400 * 7) {
|
|
|
|
|
|
// Less than 7 days
|
|
|
|
|
|
return Math.round(secondsPast / 86400) + ' 天前';
|
|
|
|
|
|
} else if (secondsPast < 86400 * 30) {
|
|
|
|
|
|
// Less than 30 days, show in weeks
|
|
|
|
|
|
return Math.round(secondsPast / (86400 * 7)) + ' 周前';
|
|
|
|
|
|
} else if (secondsPast < 86400 * 365) {
|
|
|
|
|
|
// Less than 1 year, show in months
|
|
|
|
|
|
return Math.round(secondsPast / (86400 * 30)) + ' 月前';
|
2024-07-16 18:14:27 +08:00
|
|
|
|
} else {
|
2025-07-27 13:02:57 +08:00
|
|
|
|
// More than 1 year, show full date
|
2025-05-15 23:06:19 +08:00
|
|
|
|
const date = new Date(adjustedTimestamp);
|
2024-07-16 18:14:27 +08:00
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = date.getMonth() + 1;
|
|
|
|
|
|
const day = date.getDate();
|
|
|
|
|
|
return `${year} 年 ${month} 月 ${day} 日`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 13:02:57 +08:00
|
|
|
|
const formatStandardTime = (timestamp) => {
|
|
|
|
|
|
const date = new Date(timestamp);
|
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = date.getMonth() + 1;
|
|
|
|
|
|
const day = date.getDate();
|
|
|
|
|
|
const hour = date.getHours();
|
|
|
|
|
|
const minute = date.getMinutes();
|
|
|
|
|
|
const second = date.getSeconds();
|
|
|
|
|
|
return `${year} 年 ${month} 月 ${day} 日 ${hour}:${minute}:${second}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
|
|
|
|
|
const getDatabaseInfo = () => {
|
|
|
|
|
|
const db_id = databaseId.value
|
2024-10-14 16:51:20 +08:00
|
|
|
|
if (!db_id) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2024-07-16 18:14:27 +08:00
|
|
|
|
state.lock = true
|
2025-05-26 20:39:31 +08:00
|
|
|
|
state.databaseLoading = true
|
2024-07-16 18:14:27 +08:00
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-07-22 17:29:38 +08:00
|
|
|
|
databaseApi.getDatabaseInfo(db_id)
|
2025-07-21 18:18:47 +08:00
|
|
|
|
.then(async data => {
|
2024-07-16 18:14:27 +08:00
|
|
|
|
database.value = data
|
2025-07-26 03:36:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果当前知识库类型不支持QA分割,重置相关参数
|
|
|
|
|
|
const kbType = data.kb_type?.toLowerCase();
|
|
|
|
|
|
if (kbType !== 'chroma' && kbType !== 'milvus') {
|
|
|
|
|
|
chunkParams.value.use_qa_split = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-21 18:18:47 +08:00
|
|
|
|
// 加载查询参数
|
|
|
|
|
|
await loadQueryParams()
|
2024-07-16 18:14:27 +08:00
|
|
|
|
resolve(data)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error(error)
|
2025-05-02 23:56:59 +08:00
|
|
|
|
message.error(error.message || '获取数据库信息失败')
|
2024-07-16 18:14:27 +08:00
|
|
|
|
reject(error)
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
state.lock = false
|
2025-05-26 20:39:31 +08:00
|
|
|
|
state.databaseLoading = false
|
2024-07-16 18:14:27 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deleteFile = (fileId) => {
|
2025-05-26 22:50:47 +08:00
|
|
|
|
state.lock = true
|
|
|
|
|
|
console.debug("deleteFile", databaseId.value, fileId)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
return documentApi.deleteDocument(databaseId.value, fileId)
|
2025-05-26 22:50:47 +08:00
|
|
|
|
.then(data => {
|
|
|
|
|
|
console.log(data)
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// message.success(data.message || '删除成功')
|
2025-05-26 22:50:47 +08:00
|
|
|
|
getDatabaseInfo()
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
message.error(error.message || '删除失败')
|
2025-07-02 02:38:36 +08:00
|
|
|
|
throw error
|
2025-05-26 22:50:47 +08:00
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
state.lock = false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeleteFile = (fileId) => {
|
2024-07-16 18:14:27 +08:00
|
|
|
|
console.log(fileId)
|
2025-03-31 18:05:38 +08:00
|
|
|
|
//删除提示
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '删除文件',
|
|
|
|
|
|
content: '确定要删除该文件吗?',
|
|
|
|
|
|
okText: '确认',
|
|
|
|
|
|
cancelText: '取消',
|
2025-05-26 22:50:47 +08:00
|
|
|
|
onOk: () => deleteFile(fileId),
|
2025-03-31 18:05:38 +08:00
|
|
|
|
onCancel: () => {
|
|
|
|
|
|
console.log('Cancel');
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2024-07-16 18:14:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-26 22:50:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 批量删除处理函数
|
|
|
|
|
|
const handleBatchDelete = () => {
|
|
|
|
|
|
if (!canBatchDelete.value) {
|
|
|
|
|
|
message.info('没有可删除的文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const files = database.value.files || {};
|
2025-07-27 00:30:30 +08:00
|
|
|
|
const validFileIds = selectedRowKeys.value.filter(fileId => {
|
|
|
|
|
|
const file = files[fileId];
|
|
|
|
|
|
return file && !(file.status === 'processing' || file.status === 'waiting');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (validFileIds.length === 0) {
|
|
|
|
|
|
message.info('没有可删除的文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-05-26 22:50:47 +08:00
|
|
|
|
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '批量删除文件',
|
2025-07-27 00:30:30 +08:00
|
|
|
|
content: `确定要删除选中的 ${validFileIds.length} 个文件吗?`,
|
2025-05-26 22:50:47 +08:00
|
|
|
|
okText: '确认',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
state.batchDeleting = true;
|
2025-07-27 00:30:30 +08:00
|
|
|
|
state.batchDeleteProgress.current = 0;
|
|
|
|
|
|
state.batchDeleteProgress.total = validFileIds.length;
|
|
|
|
|
|
|
|
|
|
|
|
let successCount = 0;
|
|
|
|
|
|
let failureCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示初始进度
|
|
|
|
|
|
let progressMessage = message.loading(`正在删除文件 0/${validFileIds.length}`, 0);
|
|
|
|
|
|
|
2025-05-26 22:50:47 +08:00
|
|
|
|
try {
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// 逐个删除文件以显示进度
|
|
|
|
|
|
for (let i = 0; i < validFileIds.length; i++) {
|
|
|
|
|
|
const fileId = validFileIds[i];
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteFile(fileId);
|
|
|
|
|
|
successCount++;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`删除文件 ${fileId} 失败:`, error);
|
|
|
|
|
|
failureCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新进度
|
|
|
|
|
|
state.batchDeleteProgress.current = i + 1;
|
|
|
|
|
|
progressMessage?.();
|
|
|
|
|
|
|
|
|
|
|
|
// 显示当前进度,如果不是最后一个文件
|
|
|
|
|
|
if (i + 1 < validFileIds.length) {
|
|
|
|
|
|
progressMessage = message.loading(`正在删除文件 ${i + 1}/${validFileIds.length}`, 0);
|
|
|
|
|
|
}
|
2025-05-26 22:50:47 +08:00
|
|
|
|
}
|
2025-07-27 00:30:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 显示最终结果
|
|
|
|
|
|
progressMessage?.();
|
|
|
|
|
|
|
|
|
|
|
|
if (successCount > 0 && failureCount === 0) {
|
|
|
|
|
|
message.success(`成功删除 ${successCount} 个文件`);
|
|
|
|
|
|
} else if (successCount > 0 && failureCount > 0) {
|
|
|
|
|
|
message.warning(`成功删除 ${successCount} 个文件,${failureCount} 个文件删除失败`);
|
|
|
|
|
|
} else if (failureCount > 0) {
|
|
|
|
|
|
message.error(`${failureCount} 个文件删除失败`);
|
2025-05-26 22:50:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
selectedRowKeys.value = []; // 清空选择
|
|
|
|
|
|
getDatabaseInfo(); // 刷新列表状态
|
|
|
|
|
|
} catch (error) {
|
2025-07-27 00:30:30 +08:00
|
|
|
|
progressMessage?.();
|
2025-05-26 22:50:47 +08:00
|
|
|
|
console.error('批量删除出错:', error);
|
|
|
|
|
|
message.error('批量删除过程中发生错误');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
state.batchDeleting = false;
|
2025-07-27 00:30:30 +08:00
|
|
|
|
state.batchDeleteProgress.current = 0;
|
|
|
|
|
|
state.batchDeleteProgress.total = 0;
|
2025-05-26 22:50:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-23 23:10:32 +08:00
|
|
|
|
const chunkParams = ref({
|
|
|
|
|
|
chunk_size: 1000,
|
|
|
|
|
|
chunk_overlap: 200,
|
2025-05-23 15:30:14 +08:00
|
|
|
|
enable_ocr: 'disable',
|
2025-07-26 03:36:54 +08:00
|
|
|
|
use_qa_split: false,
|
|
|
|
|
|
qa_separator: '\n\n\n',
|
2025-03-23 23:10:32 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-06-27 01:55:04 +08:00
|
|
|
|
// "生成分块" - 新的统一方法
|
|
|
|
|
|
const addFiles = (items, contentType = 'file') => {
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
|
message.error(contentType === 'file' ? '请先上传文件' : '请输入有效的网页链接');
|
|
|
|
|
|
return;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-27 01:55:04 +08:00
|
|
|
|
state.chunkLoading = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置内容类型
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
...chunkParams.value,
|
|
|
|
|
|
content_type: contentType
|
|
|
|
|
|
};
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
documentApi.addDocuments(databaseId.value, items, params)
|
2025-03-23 23:10:32 +08:00
|
|
|
|
.then(data => {
|
2025-06-27 01:55:04 +08:00
|
|
|
|
console.log('处理结果:', data);
|
2025-05-26 13:12:17 +08:00
|
|
|
|
if (data.status === 'success') {
|
2025-06-27 01:55:04 +08:00
|
|
|
|
const itemType = contentType === 'file' ? '文件' : 'URL';
|
|
|
|
|
|
message.success(data.message || `${itemType}已提交处理,请稍后在列表刷新查看状态`);
|
|
|
|
|
|
|
|
|
|
|
|
// 清空输入
|
|
|
|
|
|
if (contentType === 'file') {
|
|
|
|
|
|
fileList.value = [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
urlList.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addFilesModalVisible.value = false;
|
|
|
|
|
|
getDatabaseInfo();
|
2025-05-26 13:12:17 +08:00
|
|
|
|
} else {
|
2025-06-27 01:55:04 +08:00
|
|
|
|
message.error(data.message || '处理失败');
|
2025-05-26 13:12:17 +08:00
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
2025-06-27 01:55:04 +08:00
|
|
|
|
console.error(error);
|
|
|
|
|
|
message.error(error.message || '处理请求失败');
|
2025-03-23 23:10:32 +08:00
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
2025-06-27 01:55:04 +08:00
|
|
|
|
state.chunkLoading = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-21 18:18:47 +08:00
|
|
|
|
watch(() => route.params.database_id, async (newId) => {
|
2024-07-16 18:14:27 +08:00
|
|
|
|
databaseId.value = newId;
|
|
|
|
|
|
console.log(newId)
|
2025-06-27 01:55:04 +08:00
|
|
|
|
stopAutoRefresh();
|
2025-07-21 18:18:47 +08:00
|
|
|
|
await getDatabaseInfo();
|
|
|
|
|
|
startAutoRefresh();
|
2024-07-16 18:14:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
// 监听数据库信息变化,重置过滤条件
|
|
|
|
|
|
watch(() => database.value, () => {
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// 当数据库信息更新时,重置过滤条件和选中状态
|
2025-07-26 03:36:54 +08:00
|
|
|
|
filenameFilter.value = '';
|
|
|
|
|
|
selectedRowKeys.value = [];
|
2025-07-26 15:19:11 +08:00
|
|
|
|
// 重置排序状态
|
|
|
|
|
|
sortState.value.field = null;
|
|
|
|
|
|
sortState.value.order = null;
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// 分页重置由 filteredFiles 计算属性自动处理
|
2025-07-26 03:36:54 +08:00
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-03-23 23:10:32 +08:00
|
|
|
|
// 添加更多示例查询
|
2024-09-14 02:46:44 +08:00
|
|
|
|
const queryExamples = ref([
|
2025-06-27 01:55:04 +08:00
|
|
|
|
'孕妇应该避免吃哪些水果?',
|
2025-07-27 02:56:13 +08:00
|
|
|
|
'荔枝应该怎么清洗?',
|
|
|
|
|
|
'如何判断西瓜是否成熟?',
|
|
|
|
|
|
'苹果有哪些营养价值?',
|
|
|
|
|
|
'什么季节最适合吃梨?',
|
|
|
|
|
|
'如何保存草莓以延长保质期?',
|
|
|
|
|
|
'香蕉变黑后还能吃吗?',
|
|
|
|
|
|
'橙子皮可以用来做什么?'
|
2024-09-14 02:46:44 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
// 当前示例索引
|
|
|
|
|
|
const currentExampleIndex = ref(0);
|
|
|
|
|
|
|
2024-09-14 02:46:44 +08:00
|
|
|
|
// 使用示例查询的方法
|
|
|
|
|
|
const useQueryExample = (example) => {
|
|
|
|
|
|
queryText.value = example;
|
|
|
|
|
|
onQuery();
|
|
|
|
|
|
};
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
// 示例轮播相关
|
|
|
|
|
|
let exampleCarouselInterval = null;
|
|
|
|
|
|
const startExampleCarousel = () => {
|
|
|
|
|
|
if (exampleCarouselInterval) return;
|
|
|
|
|
|
|
|
|
|
|
|
exampleCarouselInterval = setInterval(() => {
|
|
|
|
|
|
currentExampleIndex.value = (currentExampleIndex.value + 1) % queryExamples.value.length;
|
|
|
|
|
|
}, 6000); // 每6秒切换一次
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const stopExampleCarousel = () => {
|
|
|
|
|
|
if (exampleCarouselInterval) {
|
|
|
|
|
|
clearInterval(exampleCarouselInterval);
|
|
|
|
|
|
exampleCarouselInterval = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 组件挂载时启动示例轮播
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
getDatabaseInfo();
|
|
|
|
|
|
startAutoRefresh();
|
|
|
|
|
|
// 初始化时检查OCR服务健康状态
|
|
|
|
|
|
checkOcrHealth();
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (isGraphSupported.value) {
|
|
|
|
|
|
refreshGraph();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 800);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加拖拽事件监听
|
|
|
|
|
|
if (resizeHandle.value) {
|
|
|
|
|
|
resizeHandle.value.addEventListener('mousedown', handleMouseDown);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (resizeHandleHorizontal.value) {
|
|
|
|
|
|
resizeHandleHorizontal.value.addEventListener('mousedown', handleMouseDownHorizontal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加键盘事件监听
|
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
|
|
|
|
|
|
|
// 启动示例轮播
|
|
|
|
|
|
startExampleCarousel();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时停止示例轮播
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
stopAutoRefresh();
|
|
|
|
|
|
if (resizeHandle.value) {
|
|
|
|
|
|
resizeHandle.value.removeEventListener('mousedown', handleMouseDown);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (resizeHandleHorizontal.value) {
|
|
|
|
|
|
resizeHandleHorizontal.value.removeEventListener('mousedown', handleMouseDownHorizontal);
|
|
|
|
|
|
}
|
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMoveHorizontal);
|
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUpHorizontal);
|
|
|
|
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
|
|
|
|
|
|
|
// 停止示例轮播
|
|
|
|
|
|
stopExampleCarousel();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// 拖拽调整大小功能
|
|
|
|
|
|
const handleMouseDown = () => {
|
|
|
|
|
|
isDragging.value = true;
|
|
|
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
|
|
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
|
|
|
|
document.body.style.cursor = 'col-resize';
|
|
|
|
|
|
document.body.style.userSelect = 'none';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseMove = (e) => {
|
|
|
|
|
|
if (!isDragging.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
const container = document.querySelector('.unified-layout');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
|
const newWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
|
|
|
|
|
|
|
|
|
|
// 限制最小和最大宽度
|
|
|
|
|
|
leftPanelWidth.value = Math.max(20, Math.min(60, newWidth));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseUp = () => {
|
|
|
|
|
|
isDragging.value = false;
|
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
|
|
|
|
document.body.style.cursor = '';
|
|
|
|
|
|
document.body.style.userSelect = '';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseDownHorizontal = () => {
|
|
|
|
|
|
isDraggingVertical.value = true;
|
|
|
|
|
|
document.addEventListener('mousemove', handleMouseMoveHorizontal);
|
|
|
|
|
|
document.addEventListener('mouseup', handleMouseUpHorizontal);
|
|
|
|
|
|
document.body.style.cursor = 'row-resize';
|
|
|
|
|
|
document.body.style.userSelect = 'none';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseMoveHorizontal = (e) => {
|
|
|
|
|
|
if (!isDraggingVertical.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
const container = document.querySelector('.right-panel');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
|
const newHeight = ((e.clientY - containerRect.top) / containerRect.height) * 100;
|
|
|
|
|
|
|
|
|
|
|
|
rightPanelHeight.graph = Math.max(10, Math.min(90, newHeight));
|
|
|
|
|
|
rightPanelHeight.query = 100 - rightPanelHeight.graph;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseUpHorizontal = () => {
|
|
|
|
|
|
isDraggingVertical.value = false;
|
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMoveHorizontal);
|
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUpHorizontal);
|
|
|
|
|
|
document.body.style.cursor = '';
|
|
|
|
|
|
document.body.style.userSelect = '';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
// 键盘事件处理
|
|
|
|
|
|
const handleKeyDown = (e) => {
|
|
|
|
|
|
// 当按下ESC键时清空过滤条件
|
|
|
|
|
|
if (e.key === 'Escape' && filenameFilter.value) {
|
|
|
|
|
|
filenameFilter.value = '';
|
|
|
|
|
|
selectedRowKeys.value = [];
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// 分页重置由 filteredFiles 计算属性自动处理
|
2025-07-26 03:36:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// 清理事件监听
|
2025-03-06 23:55:20 +08:00
|
|
|
|
onUnmounted(() => {
|
2025-06-27 01:55:04 +08:00
|
|
|
|
stopAutoRefresh();
|
2025-07-23 03:52:22 +08:00
|
|
|
|
if (resizeHandle.value) {
|
|
|
|
|
|
resizeHandle.value.removeEventListener('mousedown', handleMouseDown);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (resizeHandleHorizontal.value) {
|
|
|
|
|
|
resizeHandleHorizontal.value.removeEventListener('mousedown', handleMouseDownHorizontal);
|
|
|
|
|
|
}
|
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
|
|
|
|
document.removeEventListener('mousemove', handleMouseMoveHorizontal);
|
|
|
|
|
|
document.removeEventListener('mouseup', handleMouseUpHorizontal);
|
2025-07-26 03:36:54 +08:00
|
|
|
|
document.removeEventListener('keydown', handleKeyDown);
|
2025-07-23 03:52:22 +08:00
|
|
|
|
});
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
2025-05-07 00:38:33 +08:00
|
|
|
|
const uploadMode = ref('file');
|
|
|
|
|
|
const urlList = ref('');
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-05-07 00:38:33 +08:00
|
|
|
|
const chunkData = () => {
|
2025-07-21 19:25:07 +08:00
|
|
|
|
// 验证OCR服务可用性
|
|
|
|
|
|
if (!validateOcrService()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-07 00:38:33 +08:00
|
|
|
|
if (uploadMode.value === 'file') {
|
2025-07-02 02:38:36 +08:00
|
|
|
|
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path);
|
|
|
|
|
|
console.log(files);
|
|
|
|
|
|
addFiles(files, 'file');
|
2025-05-07 00:38:33 +08:00
|
|
|
|
} else if (uploadMode.value === 'url') {
|
2025-07-02 02:38:36 +08:00
|
|
|
|
const urls = urlList.value.split('\n')
|
|
|
|
|
|
.map(url => url.trim())
|
|
|
|
|
|
.filter(url => url.length > 0 && (url.startsWith('http://') || url.startsWith('https://')));
|
|
|
|
|
|
|
|
|
|
|
|
if (urls.length === 0) {
|
|
|
|
|
|
message.error('请输入有效的网页链接(必须以http://或https://开头)');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addFiles(urls, 'url');
|
2025-05-07 00:38:33 +08:00
|
|
|
|
}
|
2025-07-26 03:36:54 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
addFilesModalVisible.value = false;
|
|
|
|
|
|
getDatabaseInfo();
|
|
|
|
|
|
}, 1000);
|
2025-05-07 00:38:33 +08:00
|
|
|
|
}
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-05-09 16:11:03 +08:00
|
|
|
|
const getAuthHeaders = () => {
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
return userStore.getAuthHeaders();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-10 10:24:18 +08:00
|
|
|
|
// 编辑知识库表单
|
|
|
|
|
|
const editModalVisible = ref(false);
|
|
|
|
|
|
const editFormRef = ref(null);
|
|
|
|
|
|
const editForm = reactive({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
description: ''
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const rules = {
|
|
|
|
|
|
name: [{ required: true, message: '请输入知识库名称' }]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
// 分块参数配置弹窗
|
|
|
|
|
|
const chunkConfigModalVisible = ref(false);
|
|
|
|
|
|
const tempChunkParams = ref({
|
|
|
|
|
|
chunk_size: 1000,
|
|
|
|
|
|
chunk_overlap: 200,
|
2025-07-26 03:36:54 +08:00
|
|
|
|
use_qa_split: false,
|
|
|
|
|
|
qa_separator: '\n\n\n',
|
2025-06-18 19:07:29 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 添加文件弹窗
|
|
|
|
|
|
const addFilesModalVisible = ref(false);
|
|
|
|
|
|
|
2025-05-10 10:24:18 +08:00
|
|
|
|
// 显示编辑对话框
|
|
|
|
|
|
const showEditModal = () => {
|
|
|
|
|
|
editForm.name = database.value.name || '';
|
|
|
|
|
|
editForm.description = database.value.description || '';
|
|
|
|
|
|
editModalVisible.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 提交编辑表单
|
|
|
|
|
|
const handleEditSubmit = () => {
|
|
|
|
|
|
editFormRef.value.validate().then(() => {
|
|
|
|
|
|
updateDatabaseInfo();
|
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
|
console.error('表单验证失败:', err);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新知识库信息
|
|
|
|
|
|
const updateDatabaseInfo = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
state.lock = true;
|
2025-07-22 17:29:38 +08:00
|
|
|
|
const response = await databaseApi.updateDatabase(databaseId.value, {
|
2025-05-10 10:24:18 +08:00
|
|
|
|
name: editForm.name,
|
|
|
|
|
|
description: editForm.description
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
message.success('知识库信息更新成功');
|
|
|
|
|
|
editModalVisible.value = false;
|
|
|
|
|
|
await getDatabaseInfo(); // 刷新数据
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(error);
|
|
|
|
|
|
message.error(error.message || '更新失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
state.lock = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
// 显示分块参数配置弹窗
|
|
|
|
|
|
const showChunkConfigModal = () => {
|
|
|
|
|
|
tempChunkParams.value = {
|
|
|
|
|
|
chunk_size: chunkParams.value.chunk_size,
|
|
|
|
|
|
chunk_overlap: chunkParams.value.chunk_overlap,
|
2025-07-26 03:36:54 +08:00
|
|
|
|
use_qa_split: isQaSplitSupported.value ? chunkParams.value.use_qa_split : false,
|
|
|
|
|
|
qa_separator: chunkParams.value.qa_separator,
|
2025-06-18 19:07:29 +08:00
|
|
|
|
};
|
|
|
|
|
|
chunkConfigModalVisible.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理分块参数配置提交
|
|
|
|
|
|
const handleChunkConfigSubmit = () => {
|
|
|
|
|
|
chunkParams.value.chunk_size = tempChunkParams.value.chunk_size;
|
|
|
|
|
|
chunkParams.value.chunk_overlap = tempChunkParams.value.chunk_overlap;
|
2025-07-26 03:36:54 +08:00
|
|
|
|
// 只有支持QA分割的知识库类型才保存QA分割配置
|
|
|
|
|
|
if (isQaSplitSupported.value) {
|
|
|
|
|
|
chunkParams.value.use_qa_split = tempChunkParams.value.use_qa_split;
|
|
|
|
|
|
chunkParams.value.qa_separator = tempChunkParams.value.qa_separator;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
chunkParams.value.use_qa_split = false;
|
|
|
|
|
|
}
|
2025-06-18 19:07:29 +08:00
|
|
|
|
chunkConfigModalVisible.value = false;
|
|
|
|
|
|
message.success('分块参数配置已更新');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示添加文件弹窗
|
|
|
|
|
|
const showAddFilesModal = () => {
|
|
|
|
|
|
addFilesModalVisible.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-26 20:39:31 +08:00
|
|
|
|
|
2025-06-27 01:55:04 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-05-26 20:39:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 选中的行
|
|
|
|
|
|
const selectedRowKeys = ref([]);
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// 面板可见性控制
|
|
|
|
|
|
const panels = reactive({
|
|
|
|
|
|
query: { visible: true },
|
|
|
|
|
|
graph: { visible: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const togglePanel = (panel) => {
|
|
|
|
|
|
panels[panel].visible = !panels[panel].visible;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-26 15:19:11 +08:00
|
|
|
|
// 切换右侧面板显示/隐藏
|
|
|
|
|
|
const toggleRightPanel = () => {
|
|
|
|
|
|
state.rightPanelVisible = !state.rightPanelVisible;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// 拖拽调整大小
|
|
|
|
|
|
const leftPanelWidth = ref(45); // 百分比
|
|
|
|
|
|
const isDragging = ref(false);
|
|
|
|
|
|
const resizeHandle = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const rightPanelHeight = reactive({ query: 50, graph: 50 });
|
|
|
|
|
|
const isDraggingVertical = ref(false);
|
|
|
|
|
|
const resizeHandleHorizontal = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算面板样式的方法
|
|
|
|
|
|
const computePanelStyles = () => {
|
|
|
|
|
|
const queryVisible = panels.query.visible;
|
|
|
|
|
|
const graphVisible = panels.graph.visible;
|
|
|
|
|
|
|
|
|
|
|
|
if (queryVisible && graphVisible) {
|
|
|
|
|
|
// 两个面板都显示时,按比例分配
|
|
|
|
|
|
return {
|
|
|
|
|
|
query: { height: rightPanelHeight.query + '%' },
|
|
|
|
|
|
graph: { height: rightPanelHeight.graph + '%' }
|
|
|
|
|
|
};
|
|
|
|
|
|
} else if (queryVisible && !graphVisible) {
|
|
|
|
|
|
// 只显示查询面板时,查询面板占满
|
|
|
|
|
|
return {
|
|
|
|
|
|
query: { height: 'calc(100% - 40px)' },
|
|
|
|
|
|
graph: { height: '36px' }
|
|
|
|
|
|
};
|
|
|
|
|
|
} else if (!queryVisible && graphVisible) {
|
|
|
|
|
|
// 只显示图谱面板时,图谱面板占满
|
|
|
|
|
|
return {
|
|
|
|
|
|
query: { height: '36px' },
|
|
|
|
|
|
graph: { height: 'calc(100% - 40px)' }
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 两个面板都折叠时
|
|
|
|
|
|
return {
|
|
|
|
|
|
query: { height: '36px' },
|
|
|
|
|
|
graph: { height: '36px' }
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 紧凑表格列定义
|
|
|
|
|
|
const columnsCompact = [
|
2025-07-26 15:19:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '文件名',
|
|
|
|
|
|
dataIndex: 'filename',
|
|
|
|
|
|
key: 'filename',
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
width: undefined, // 不设置宽度,让它占据剩余空间
|
|
|
|
|
|
sorter: (a, b) => (a.filename || '').localeCompare(b.filename || ''),
|
|
|
|
|
|
sortDirections: ['ascend', 'descend']
|
|
|
|
|
|
},
|
2025-07-27 13:02:57 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '时间',
|
|
|
|
|
|
dataIndex: 'created_at',
|
|
|
|
|
|
key: 'created_at',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
align: 'right',
|
|
|
|
|
|
sorter: (a, b) => (a.created_at || 0) - (b.created_at || 0),
|
|
|
|
|
|
sortDirections: ['ascend', 'descend']
|
|
|
|
|
|
},
|
2025-07-26 15:19:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
width: 60,
|
|
|
|
|
|
align: 'right',
|
|
|
|
|
|
sorter: (a, b) => {
|
|
|
|
|
|
const statusOrder = { 'done': 1, 'processing': 2, 'waiting': 3, 'failed': 4 };
|
|
|
|
|
|
return (statusOrder[a.status] || 5) - (statusOrder[b.status] || 5);
|
|
|
|
|
|
},
|
|
|
|
|
|
sortDirections: ['ascend', 'descend']
|
|
|
|
|
|
},
|
2025-07-23 03:52:22 +08:00
|
|
|
|
{ title: '', key: 'action', dataIndex: 'file_id', width: 40, align: 'center' }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
// 过滤后的文件列表
|
|
|
|
|
|
const filteredFiles = computed(() => {
|
|
|
|
|
|
const files = Object.values(database.value.files || {});
|
2025-07-26 15:19:11 +08:00
|
|
|
|
let filtered = files;
|
|
|
|
|
|
|
|
|
|
|
|
// 应用文件名过滤
|
|
|
|
|
|
if (filenameFilter.value.trim()) {
|
|
|
|
|
|
const filterText = filenameFilter.value.toLowerCase().trim();
|
|
|
|
|
|
filtered = files.filter(file =>
|
|
|
|
|
|
file.filename && file.filename.toLowerCase().includes(filterText)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 应用排序
|
|
|
|
|
|
if (sortState.value.field && sortState.value.order) {
|
|
|
|
|
|
filtered = [...filtered].sort((a, b) => {
|
|
|
|
|
|
let result = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (sortState.value.field === 'status') {
|
|
|
|
|
|
const statusOrder = { 'done': 1, 'processing': 2, 'waiting': 3, 'failed': 4 };
|
|
|
|
|
|
result = (statusOrder[a.status] || 5) - (statusOrder[b.status] || 5);
|
|
|
|
|
|
} else if (sortState.value.field === 'filename') {
|
|
|
|
|
|
result = (a.filename || '').localeCompare(b.filename || '');
|
|
|
|
|
|
} else if (sortState.value.field === 'created_at') {
|
|
|
|
|
|
result = (a.created_at || 0) - (b.created_at || 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return sortState.value.order === 'descend' ? -result : result;
|
|
|
|
|
|
});
|
2025-07-26 03:36:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// 同步分页状态 - 确保当前页码在有效范围内
|
|
|
|
|
|
const totalPages = Math.ceil(filtered.length / paginationCompact.value.pageSize);
|
|
|
|
|
|
if (totalPages > 0 && paginationCompact.value.current > totalPages) {
|
|
|
|
|
|
// 如果当前页超出范围,调整到最后一页
|
|
|
|
|
|
paginationCompact.value.current = totalPages;
|
|
|
|
|
|
} else if (filtered.length === 0) {
|
|
|
|
|
|
// 如果没有数据,重置到第一页
|
|
|
|
|
|
paginationCompact.value.current = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-26 15:19:11 +08:00
|
|
|
|
return filtered;
|
2025-07-26 03:36:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 空状态文本
|
|
|
|
|
|
const emptyText = computed(() => {
|
|
|
|
|
|
return filenameFilter.value ? `没有找到包含"${filenameFilter.value}"的文件` : '暂无文件';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-26 15:19:11 +08:00
|
|
|
|
// 排序状态
|
|
|
|
|
|
const sortState = ref({
|
|
|
|
|
|
field: null,
|
|
|
|
|
|
order: null
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// 紧凑分页配置
|
|
|
|
|
|
const paginationCompact = ref({
|
|
|
|
|
|
pageSize: 20,
|
|
|
|
|
|
current: 1,
|
2025-07-26 15:19:11 +08:00
|
|
|
|
total: 0,
|
2025-07-23 03:52:22 +08:00
|
|
|
|
showSizeChanger: false,
|
|
|
|
|
|
showTotal: (total) => `${total}`,
|
|
|
|
|
|
size: 'small',
|
|
|
|
|
|
showQuickJumper: false,
|
2025-07-27 00:30:30 +08:00
|
|
|
|
onChange: (page, pageSize) => {
|
|
|
|
|
|
paginationCompact.value.current = page;
|
|
|
|
|
|
paginationCompact.value.pageSize = pageSize;
|
|
|
|
|
|
// 清空选中的行,因为分页后选中的行可能不在当前视图中
|
|
|
|
|
|
selectedRowKeys.value = [];
|
|
|
|
|
|
},
|
2025-07-23 03:52:22 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-26 15:19:11 +08:00
|
|
|
|
// 监听过滤后的文件列表变化,更新分页总数
|
|
|
|
|
|
watch(filteredFiles, (newFiles) => {
|
|
|
|
|
|
paginationCompact.value.total = newFiles.length;
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// filteredFiles 计算属性已经处理了分页范围检查,这里不需要重复处理
|
2025-07-26 15:19:11 +08:00
|
|
|
|
}, { immediate: true });
|
|
|
|
|
|
|
2025-05-26 20:39:31 +08:00
|
|
|
|
// 行选择改变处理
|
|
|
|
|
|
const onSelectChange = (keys) => {
|
|
|
|
|
|
selectedRowKeys.value = keys;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取复选框属性
|
|
|
|
|
|
const getCheckboxProps = (record) => ({
|
2025-06-27 01:55:04 +08:00
|
|
|
|
disabled: state.lock || record.status === 'processing' || record.status === 'waiting',
|
2025-05-26 20:39:31 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-26 15:19:11 +08:00
|
|
|
|
// 排序事件处理
|
2025-07-27 00:30:30 +08:00
|
|
|
|
const handleTableChange = (_, __, sorter) => {
|
2025-07-26 15:19:11 +08:00
|
|
|
|
// 处理排序
|
|
|
|
|
|
if (sorter && sorter.field) {
|
|
|
|
|
|
sortState.value.field = sorter.field;
|
|
|
|
|
|
sortState.value.order = sorter.order;
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// 清空选中的行,因为排序后选中的行可能不在当前视图中
|
|
|
|
|
|
selectedRowKeys.value = [];
|
|
|
|
|
|
// 不再强制重置到第一页,让 filteredFiles 计算属性自动处理分页调整
|
2025-07-26 15:19:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
// 过滤相关方法
|
|
|
|
|
|
const onFilterChange = (e) => {
|
|
|
|
|
|
filenameFilter.value = e.target.value;
|
|
|
|
|
|
// 清空选中的行,因为过滤后选中的行可能不在当前视图中
|
|
|
|
|
|
selectedRowKeys.value = [];
|
2025-07-26 15:19:11 +08:00
|
|
|
|
// 重置排序状态
|
|
|
|
|
|
sortState.value.field = null;
|
|
|
|
|
|
sortState.value.order = null;
|
2025-07-27 00:30:30 +08:00
|
|
|
|
// 分页调整由 filteredFiles 计算属性自动处理
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 选择所有失败的文件
|
|
|
|
|
|
const selectAllFailedFiles = () => {
|
|
|
|
|
|
const failedFiles = filteredFiles.value
|
|
|
|
|
|
.filter(file => file.status === 'failed')
|
|
|
|
|
|
.map(file => file.file_id);
|
|
|
|
|
|
|
|
|
|
|
|
// 合并当前选中的文件ID和失败的文件ID,去重
|
|
|
|
|
|
const newSelectedKeys = [...new Set([...selectedRowKeys.value, ...failedFiles])];
|
|
|
|
|
|
selectedRowKeys.value = newSelectedKeys;
|
|
|
|
|
|
|
|
|
|
|
|
if (failedFiles.length > 0) {
|
|
|
|
|
|
message.success(`已选择 ${failedFiles.length} 个失败的文件`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.info('当前没有失败的文件');
|
|
|
|
|
|
}
|
2025-07-26 03:36:54 +08:00
|
|
|
|
};
|
2025-05-26 20:39:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算是否可以批量删除
|
|
|
|
|
|
const canBatchDelete = computed(() => {
|
|
|
|
|
|
return selectedRowKeys.value.some(key => {
|
2025-07-26 03:36:54 +08:00
|
|
|
|
const file = filteredFiles.value.find(f => f.file_id === key);
|
|
|
|
|
|
return file && !(state.lock || file.status === 'processing' || file.status === 'waiting');
|
2025-05-26 20:39:31 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-27 01:55:04 +08:00
|
|
|
|
// 开始自动刷新
|
|
|
|
|
|
const startAutoRefresh = () => {
|
|
|
|
|
|
if (state.autoRefresh && !state.refreshInterval) {
|
|
|
|
|
|
state.refreshInterval = setInterval(() => {
|
|
|
|
|
|
getDatabaseInfo();
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-05-26 20:39:31 +08:00
|
|
|
|
|
2025-06-27 01:55:04 +08:00
|
|
|
|
// 停止自动刷新
|
|
|
|
|
|
const stopAutoRefresh = () => {
|
|
|
|
|
|
if (state.refreshInterval) {
|
|
|
|
|
|
clearInterval(state.refreshInterval);
|
|
|
|
|
|
state.refreshInterval = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-05-26 20:39:31 +08:00
|
|
|
|
|
2025-06-27 01:55:04 +08:00
|
|
|
|
// 切换自动刷新状态
|
2025-07-27 02:56:13 +08:00
|
|
|
|
const toggleAutoRefresh = () => {
|
|
|
|
|
|
state.autoRefresh = !state.autoRefresh; // Toggle the state directly
|
|
|
|
|
|
if (state.autoRefresh) {
|
2025-06-27 01:55:04 +08:00
|
|
|
|
startAutoRefresh();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
stopAutoRefresh();
|
2025-05-26 20:39:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
// 获取状态文本
|
|
|
|
|
|
const getStatusText = (status) => {
|
|
|
|
|
|
const statusMap = {
|
|
|
|
|
|
'done': '处理完成',
|
|
|
|
|
|
'failed': '处理失败',
|
|
|
|
|
|
'processing': '处理中',
|
|
|
|
|
|
'waiting': '等待处理'
|
|
|
|
|
|
}
|
|
|
|
|
|
return statusMap[status] || status
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新图谱
|
|
|
|
|
|
const refreshGraph = () => {
|
|
|
|
|
|
if (graphViewerRef.value) {
|
|
|
|
|
|
graphViewerRef.value.loadFullGraph();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 清空图谱
|
|
|
|
|
|
const clearGraph = () => {
|
|
|
|
|
|
if (graphViewerRef.value) {
|
|
|
|
|
|
graphViewerRef.value.clearGraph();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-21 18:18:47 +08:00
|
|
|
|
// 知识库类型相关工具方法
|
|
|
|
|
|
const getKbTypeLabel = (type) => {
|
|
|
|
|
|
const labels = {
|
|
|
|
|
|
lightrag: 'LightRAG',
|
|
|
|
|
|
chroma: 'Chroma',
|
|
|
|
|
|
milvus: 'Milvus'
|
|
|
|
|
|
}
|
|
|
|
|
|
return labels[type] || type
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getKbTypeIcon = (type) => {
|
|
|
|
|
|
const icons = {
|
|
|
|
|
|
lightrag: Database,
|
|
|
|
|
|
chroma: Zap,
|
|
|
|
|
|
milvus: ThunderboltOutlined
|
|
|
|
|
|
}
|
|
|
|
|
|
return icons[type] || Database
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getKbTypeColor = (type) => {
|
|
|
|
|
|
const colors = {
|
|
|
|
|
|
lightrag: 'purple',
|
|
|
|
|
|
chroma: 'orange',
|
|
|
|
|
|
milvus: 'red'
|
|
|
|
|
|
}
|
|
|
|
|
|
return colors[type] || 'blue'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// 根据文件扩展名获取文件图标
|
|
|
|
|
|
const getFileIcon = (filename) => {
|
|
|
|
|
|
if (!filename) return FileUnknownFilled
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
const extension = filename.toLowerCase().split('.').pop()
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
const iconMap = {
|
|
|
|
|
|
// 文本文件
|
|
|
|
|
|
'txt': FileTextFilled,
|
|
|
|
|
|
'text': FileTextFilled,
|
|
|
|
|
|
'log': FileTextFilled,
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// Markdown文件
|
|
|
|
|
|
'md': FileMarkdownFilled,
|
|
|
|
|
|
'markdown': FileMarkdownFilled,
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// PDF文件
|
|
|
|
|
|
'pdf': FilePdfFilled,
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// Word文档
|
|
|
|
|
|
'doc': FileWordFilled,
|
|
|
|
|
|
'docx': FileWordFilled,
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// Excel文档
|
|
|
|
|
|
'xls': FileExcelFilled,
|
|
|
|
|
|
'xlsx': FileExcelFilled,
|
|
|
|
|
|
'csv': FileExcelFilled,
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// 图片文件
|
|
|
|
|
|
'jpg': FileImageFilled,
|
|
|
|
|
|
'jpeg': FileImageFilled,
|
|
|
|
|
|
'png': FileImageFilled,
|
|
|
|
|
|
'gif': FileImageFilled,
|
|
|
|
|
|
'bmp': FileImageFilled,
|
|
|
|
|
|
'svg': FileImageFilled,
|
|
|
|
|
|
'webp': FileImageFilled,
|
|
|
|
|
|
}
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
return iconMap[extension] || FileUnknownFilled
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据文件扩展名获取文件图标颜色
|
|
|
|
|
|
const getFileIconColor = (filename) => {
|
|
|
|
|
|
if (!filename) return '#8c8c8c'
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
const extension = filename.toLowerCase().split('.').pop()
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
const colorMap = {
|
|
|
|
|
|
// 文本文件 - 蓝色
|
|
|
|
|
|
'txt': '#1890ff',
|
|
|
|
|
|
'text': '#1890ff',
|
|
|
|
|
|
'log': '#1890ff',
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// Markdown文件 - 深蓝色
|
|
|
|
|
|
'md': '#0050b3',
|
|
|
|
|
|
'markdown': '#0050b3',
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// PDF文件 - 红色
|
|
|
|
|
|
'pdf': '#ff4d4f',
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// Word文档 - 深蓝色
|
|
|
|
|
|
'doc': '#2f54eb',
|
|
|
|
|
|
'docx': '#2f54eb',
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// Excel文档 - 绿色
|
|
|
|
|
|
'xls': '#52c41a',
|
|
|
|
|
|
'xlsx': '#52c41a',
|
|
|
|
|
|
'csv': '#52c41a',
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
// 图片文件 - 紫色
|
|
|
|
|
|
'jpg': '#722ed1',
|
|
|
|
|
|
'jpeg': '#722ed1',
|
|
|
|
|
|
'png': '#722ed1',
|
|
|
|
|
|
'gif': '#722ed1',
|
|
|
|
|
|
'bmp': '#722ed1',
|
|
|
|
|
|
'svg': '#722ed1',
|
|
|
|
|
|
'webp': '#722ed1',
|
|
|
|
|
|
}
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-26 11:18:54 +08:00
|
|
|
|
return colorMap[extension] || '#8c8c8c'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-26 13:12:17 +08:00
|
|
|
|
|
2024-07-16 18:14:27 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
2025-07-26 03:36:54 +08:00
|
|
|
|
.database-info-header {
|
|
|
|
|
|
padding: 8px 16px 6px 16px;
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-14 02:46:44 +08:00
|
|
|
|
.database-info {
|
|
|
|
|
|
margin: 8px 0 0;
|
2025-07-21 18:18:47 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
|
|
|
|
.kb-type-tag {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-07-21 18:18:47 +08:00
|
|
|
|
.type-icon {
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.header-info {
|
2024-09-14 02:46:44 +08:00
|
|
|
|
display: flex;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
align-items: center;
|
2025-07-23 12:54:36 +08:00
|
|
|
|
gap: 8px;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
margin-left: auto; /* Push to the right */
|
|
|
|
|
|
|
2025-07-23 12:54:36 +08:00
|
|
|
|
span {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.kb-type-tag {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
2024-07-17 18:52:20 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.type-icon {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
width: 12px;
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.db-id,
|
|
|
|
|
|
.file-count {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
2025-06-30 22:29:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.db-main-container {
|
2024-09-14 02:46:44 +08:00
|
|
|
|
display: flex;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
2024-07-29 01:00:02 +08:00
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.my-table {
|
|
|
|
|
|
button.ant-btn-link {
|
2024-09-14 02:46:44 +08:00
|
|
|
|
padding: 0;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.span-type {
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
background: #005F77;
|
|
|
|
|
|
}
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.pdf {
|
|
|
|
|
|
background: #005F77;
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.txt {
|
|
|
|
|
|
background: #068033;
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.docx,
|
|
|
|
|
|
.doc {
|
|
|
|
|
|
background: #2C59B7;
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.md {
|
|
|
|
|
|
background: #020817;
|
|
|
|
|
|
}
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
button.del-btn {
|
|
|
|
|
|
cursor: pointer;
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
color: var(--error-color);
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&:disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
2024-07-29 01:00:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
}
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.file-detail-content {
|
|
|
|
|
|
max-height: 60vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
// padding: 0 10px;
|
2024-07-29 01:00:02 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.file-info-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 100px 1fr;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
background-color: var(--gray-100);
|
|
|
|
|
|
border-radius: 6px;
|
2024-07-29 01:00:02 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.info-item {
|
|
|
|
|
|
display: contents;
|
2025-06-27 01:55:04 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
label {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--gray-700);
|
|
|
|
|
|
font-size: 13px;
|
2025-06-27 01:55:04 +08:00
|
|
|
|
}
|
2024-07-29 01:00:02 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
span {
|
|
|
|
|
|
font-size: 13px;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
color: var(--gray-800);
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
2024-07-29 01:00:02 +08:00
|
|
|
|
}
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.status-badge {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 500;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&.done {
|
|
|
|
|
|
color: #52c41a;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
2024-09-03 16:37:59 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&.failed {
|
|
|
|
|
|
color: #ff4d4f;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
2024-09-03 16:37:59 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&.processing {
|
|
|
|
|
|
color: #1890ff;
|
2024-09-03 16:37:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&.waiting {
|
|
|
|
|
|
color: #faad14;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
2024-07-16 18:14:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.file-content-section {
|
|
|
|
|
|
margin-top: 8px;
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
h4 {
|
|
|
|
|
|
margin: 0 0 12px 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
}
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.content-lines {
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
border: 1px solid var(--gray-200);
|
|
|
|
|
|
border-radius: 4px;
|
2024-07-16 18:14:27 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.content-line {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-bottom: 1px solid var(--gray-100);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.5;
|
2025-02-28 02:41:57 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
2025-02-28 02:41:57 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--gray-50);
|
|
|
|
|
|
}
|
2025-02-28 02:41:57 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.line-number {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
|
}
|
2025-02-28 02:41:57 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.line-text {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-16 18:14:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.empty-content {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
color: var(--gray-500);
|
2024-07-16 18:14:27 +08:00
|
|
|
|
}
|
2025-03-12 21:15:04 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.loading-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-height: 100px;
|
|
|
|
|
|
}
|
2025-05-26 13:59:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-12 21:15:04 +08:00
|
|
|
|
.custom-class .line-text {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 4px;
|
2025-05-26 13:59:05 +08:00
|
|
|
|
margin: 8px 0;
|
2025-06-26 02:13:46 +08:00
|
|
|
|
background-color: var(--gray-100);
|
2025-03-20 19:51:46 +08:00
|
|
|
|
|
2025-03-12 21:15:04 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.add-files-content {
|
|
|
|
|
|
.upload-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
gap: 20px;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.source-selector {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.upload-mode-selector {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px 16px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background-color: var(--main-light-4);
|
|
|
|
|
|
border: 1px solid var(--main-light-3);
|
|
|
|
|
|
transition: all 0.2s ease;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
&.active {
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
background-color: var(--main-10);
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
}
|
2025-06-18 19:07:29 +08:00
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.config-controls {
|
|
|
|
|
|
.ant-btn {
|
|
|
|
|
|
border-color: var(--main-light-3);
|
|
|
|
|
|
color: var(--gray-700);
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
color: var(--main-color);
|
2025-03-23 23:10:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-18 19:07:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.ocr-config {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
background-color: var(--main-light-6);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid var(--main-light-3);
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.ant-form-item {
|
|
|
|
|
|
margin-bottom: 0;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.ant-form-item-label {
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
font-weight: 500;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
}
|
2025-06-18 19:07:29 +08:00
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.param-description {
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-left: 12px;
|
|
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
|
|
|
|
|
.ocr-controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ocr-status-info {
|
|
|
|
|
|
font-size: 11px; /* Reduced from 12px */
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ocr-warning {
|
|
|
|
|
|
color: #faad14;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ocr-healthy {
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
2025-06-18 19:07:29 +08:00
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
.qa-split-config {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
background-color: var(--main-light-6);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid var(--main-light-3);
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item-label {
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle-controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-description {
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-input {
|
|
|
|
|
|
border-color: var(--main-light-3);
|
|
|
|
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
|
|
&:focus {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-switch {
|
|
|
|
|
|
&.ant-switch-checked {
|
|
|
|
|
|
background-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.upload {
|
|
|
|
|
|
margin-bottom: 20px;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.upload-dragger {
|
|
|
|
|
|
margin: 0px;
|
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.url-input {
|
|
|
|
|
|
margin-bottom: 20px;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.ant-textarea {
|
|
|
|
|
|
border-color: var(--main-light-3);
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.ant-textarea:hover,
|
|
|
|
|
|
.ant-textarea:focus {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.url-hint {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
line-height: 1.5;
|
2025-03-23 23:10:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-18 19:07:29 +08:00
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.chunk-config-content {
|
|
|
|
|
|
.params-info {
|
|
|
|
|
|
background-color: var(--main-light-4);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
margin-bottom: 16px;
|
2025-05-26 22:50:47 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
color: var(--gray-700);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-26 22:50:47 +08:00
|
|
|
|
|
2025-06-18 19:07:29 +08:00
|
|
|
|
.ant-form-item {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item-label {
|
|
|
|
|
|
padding-bottom: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
label {
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 15px;
|
2025-05-26 22:50:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
}
|
2025-06-18 19:07:29 +08:00
|
|
|
|
|
|
|
|
|
|
.ant-input-number {
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&:hover,
|
|
|
|
|
|
&:focus {
|
2025-06-18 19:07:29 +08:00
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-description {
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2025-03-23 23:10:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-26 22:50:47 +08:00
|
|
|
|
.ant-modal-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
2025-06-27 01:55:04 +08:00
|
|
|
|
|
|
|
|
|
|
.auto-refresh-control {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
color: var(--gray-700);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-switch {
|
|
|
|
|
|
&.ant-switch-checked {
|
|
|
|
|
|
background-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
/* Unified Layout Styles */
|
|
|
|
|
|
.unified-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
height: calc(100vh - 54px); /* Adjust based on actual header height */
|
|
|
|
|
|
gap: 0;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.left-panel,
|
|
|
|
|
|
.right-panel {
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.left-panel {
|
|
|
|
|
|
flex-shrink: 0;
|
2025-07-26 15:19:11 +08:00
|
|
|
|
flex-grow: 1;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
border-right: 1px solid var(--gray-200);
|
|
|
|
|
|
background-color: var(--bg-sider);
|
|
|
|
|
|
padding: 8px;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.right-panel {
|
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
|
overflow-y: auto;
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.resize-handle,
|
|
|
|
|
|
.resize-handle-horizontal {
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
cursor: col-resize;
|
|
|
|
|
|
background-color: var(--gray-200);
|
|
|
|
|
|
transition: background-color 0.2s ease;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 10;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-2);
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.resize-handle-horizontal {
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
cursor: row-resize;
|
|
|
|
|
|
background-color: var(--gray-200);
|
|
|
|
|
|
transition: background-color 0.2s ease;
|
|
|
|
|
|
z-index: 10;
|
2025-07-21 19:25:07 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-21 19:25:07 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.panel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
padding: 4px 2px;
|
2025-07-21 19:25:07 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--gray-800);
|
2025-07-21 19:25:07 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
&.left-panel-title {
|
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-21 19:25:07 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.panel-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
.search-container {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
.search-hint {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: -60px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-input-search {
|
|
|
|
|
|
.ant-input {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
|
|
&:focus {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-btn {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
border-radius: 0 4px 4px 0;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.ant-btn {
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-5);
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-18 21:03:12 +08:00
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.batch-actions-compact {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
|
background-color: var(--main-light-5);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
2025-07-27 00:30:30 +08:00
|
|
|
|
.batch-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
span {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--gray-700);
|
2024-09-18 21:03:12 +08:00
|
|
|
|
}
|
2025-07-27 00:30:30 +08:00
|
|
|
|
|
|
|
|
|
|
.ant-btn {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
padding: 0 6px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-4);
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.my-table-compact {
|
|
|
|
|
|
flex: 1;
|
2025-07-26 15:19:11 +08:00
|
|
|
|
overflow: auto;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
background-color: transparent;
|
2025-07-26 15:19:11 +08:00
|
|
|
|
min-height: 0; /* 让 flex 子项可以正确缩小 */
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
|
|
|
|
|
.main-btn {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
2024-09-18 21:03:12 +08:00
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
|
|
|
|
|
.del-btn {
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
color: var(--error-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
2024-10-24 20:08:23 +08:00
|
|
|
|
}
|
2024-09-18 21:03:12 +08:00
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
|
|
|
|
|
.span-type {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 1px 5px;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
}
|
2024-09-14 02:46:44 +08:00
|
|
|
|
}
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.section-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
border-bottom: 1px solid var(--gray-200);
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
flex-shrink: 0;
|
2025-07-26 03:36:54 +08:00
|
|
|
|
background-color: var(--gray-100);
|
2025-07-23 03:52:22 +08:00
|
|
|
|
backdrop-filter: blur(4px);
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
2025-07-18 11:29:12 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.graph-stats {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
.ant-btn {
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-5);
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.graph-disabled {
|
2025-07-21 18:18:47 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
background-color: var(--gray-50);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
.disabled-content {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
|
|
|
|
|
|
h4 {
|
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
|
margin: 4px 0;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.query-section {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.query-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.query-input-row {
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
2025-07-27 02:56:13 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
.compact-query-textarea {
|
2025-07-23 03:52:22 +08:00
|
|
|
|
width: 100%;
|
2025-07-27 02:56:13 +08:00
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
border: 1px solid var(--gray-300);
|
|
|
|
|
|
box-shadow: 0 0 4px 0px rgba(2, 10, 15, 0.05);
|
|
|
|
|
|
transition: border-color 0.2s ease;
|
|
|
|
|
|
padding: 8px 12px;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
&:focus {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
box-shadow: 0 0 4px 0px rgba(2, 10, 15, 0.05);
|
2025-07-23 03:52:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
:deep(.ant-input) {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-input:focus) {
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
2025-07-23 03:52:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
.query-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-button {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
padding: 4px 16px;
|
|
|
|
|
|
border-color: var(--gray-300);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.query-examples-compact {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-07-27 02:56:13 +08:00
|
|
|
|
gap: 6px;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
.examples-label {
|
|
|
|
|
|
font-size: 12px;
|
2025-07-21 18:18:47 +08:00
|
|
|
|
color: var(--gray-600);
|
2025-07-27 02:56:13 +08:00
|
|
|
|
white-space: nowrap;
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
.examples-container {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
|
font-size: 13px;
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
.example-btn {
|
|
|
|
|
|
background-color: var(--gray-100);
|
|
|
|
|
|
border: 1px solid var(--gray-200);
|
|
|
|
|
|
color: var(--gray-700);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-5);
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
color: var(--main-400);
|
|
|
|
|
|
}
|
2025-07-23 03:52:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
// 轮播动画
|
|
|
|
|
|
.fade-enter-active, .fade-leave-active {
|
|
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.fade-enter-from, .fade-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
|
|
|
|
|
|
.query-results {
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
background-color: var(--gray-50);
|
|
|
|
|
|
border: 1px solid var(--gray-200);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
flex: 1; /* 自动占据剩余所有空间 */
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
min-height: 0; /* 允许 flex 项目缩小 */
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.graph-section {
|
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
|
|
|
|
|
|
|
&.collapsed {
|
|
|
|
|
|
flex-grow: 0;
|
|
|
|
|
|
}
|
2025-07-18 11:29:12 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
.graph-container-compact {
|
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
padding-top: 0;
|
2025-06-30 22:29:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-09 17:07:03 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
/* Improve panel transitions */
|
|
|
|
|
|
.query-section,
|
|
|
|
|
|
.graph-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border-top: 1px solid var(--gray-300);
|
|
|
|
|
|
|
|
|
|
|
|
&.collapsed {
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
min-height: 36px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content {
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Improve the resize handle visibility */
|
|
|
|
|
|
.resize-handle,
|
|
|
|
|
|
.resize-handle-horizontal {
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
background-color: var(--main-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Table row selection styling */
|
|
|
|
|
|
:deep(.ant-table-tbody > tr.ant-table-row-selected > td) {
|
|
|
|
|
|
background-color: var(--main-light-5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-tbody > tr:hover > td) {
|
|
|
|
|
|
background-color: var(--main-light-6);
|
|
|
|
|
|
}
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
|
|
|
|
|
.panel-action-btn {
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: 1px solid var(--gray-300);
|
|
|
|
|
|
background-color: var(--gray-50);
|
|
|
|
|
|
color: var(--gray-700);
|
2025-07-27 02:56:13 +08:00
|
|
|
|
transition: all 0.1s ease;
|
|
|
|
|
|
|
|
|
|
|
|
&.expanded {
|
|
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transform: scaleX(-1);
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.auto-refresh-btn {
|
|
|
|
|
|
&.ant-btn-primary {
|
2025-07-27 04:12:49 +08:00
|
|
|
|
background-color: var(--main-color);
|
2025-07-27 02:56:13 +08:00
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
color: #fff;
|
2025-07-26 15:19:11 +08:00
|
|
|
|
|
2025-07-27 04:12:49 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-color) !important;
|
|
|
|
|
|
color: #fff !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-26 15:19:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 02:56:13 +08:00
|
|
|
|
&.active.expanded {
|
2025-07-26 15:19:11 +08:00
|
|
|
|
transform: scaleX(1);
|
2025-07-27 04:12:49 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: var(--main-light-5);
|
|
|
|
|
|
border-color: var(--main-color);
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
}
|
2025-07-26 15:19:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-09 17:07:03 +08:00
|
|
|
|
</style>
|
2025-03-23 23:10:32 +08:00
|
|
|
|
|
2025-07-23 03:52:22 +08:00
|
|
|
|
<style lang="less">
|
|
|
|
|
|
:deep(.full-modal) {
|
|
|
|
|
|
.ant-modal {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-modal-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: calc(100vh - 200px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-modal-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.maximized-graph-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.maximized-graph-content {
|
|
|
|
|
|
height: calc(100vh - 300px);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 全局样式作为备用方案 */
|
|
|
|
|
|
.ant-popover .query-params-compact {
|
|
|
|
|
|
width: 220px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-popover .query-params-compact .params-loading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-height: 80px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-popover .query-params-compact .params-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-popover .query-params-compact .param-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-popover .query-params-compact .param-item label {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--gray-700);
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</style>
|