2026-02-02 17:47:11 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="knowledge-page">
|
|
|
|
|
|
<!-- 数据集列表页 -->
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="knowledge-list-view" v-if="!currentknowledge">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<el-icon class="header-icon"><ele-Folder /></el-icon>
|
|
|
|
|
|
<span class="header-title">知识库</span>
|
|
|
|
|
|
</div>
|
2026-04-16 14:49:33 +08:00
|
|
|
|
<div class="header-actions">
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" v-debounce @click="onAddknowledge">
|
2026-04-16 14:49:33 +08:00
|
|
|
|
<el-icon><ele-Plus /></el-icon>
|
|
|
|
|
|
新建知识库
|
|
|
|
|
|
</el-button>
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="success" v-debounce @click="onOpenModelConfig">
|
2026-04-16 14:49:33 +08:00
|
|
|
|
<el-icon><ele-Setting /></el-icon>
|
|
|
|
|
|
模型配置
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="knowledge-cards" v-loading="knowledgeLoading">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 数据集卡片 -->
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="knowledge-card"
|
|
|
|
|
|
v-for="item in knowledgeList"
|
2026-02-02 17:47:11 +08:00
|
|
|
|
:key="item.id"
|
2026-03-30 17:35:05 +08:00
|
|
|
|
@click="onSelectknowledge(item)"
|
2026-02-02 17:47:11 +08:00
|
|
|
|
@contextmenu.prevent="onCardContextMenu($event, item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="card-icon">
|
|
|
|
|
|
<span class="icon-text">{{ item.name?.charAt(0)?.toUpperCase() || 'D' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="card-info">
|
|
|
|
|
|
<div class="card-name">{{ item.name }}</div>
|
2026-04-13 15:16:08 +08:00
|
|
|
|
<div class="card-meta">{{ item.documentCount || 0 }} 个文件</div>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<div class="card-time">{{ item.createdAt }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 悬停操作按钮 -->
|
|
|
|
|
|
<div class="card-actions" @click.stop>
|
|
|
|
|
|
<el-tooltip content="重命名" placement="top">
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button text size="small" v-debounce @click="onRenameknowledge(item)">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-icon><ele-Edit /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
<el-tooltip content="删除" placement="top">
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button text size="small" type="danger" v-debounce @click="onDeleteknowledge(item)">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-icon><ele-Delete /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 查看全部卡片 -->
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="see-all-card" v-if="knowledgeList.length > 0">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<span>See All</span>
|
|
|
|
|
|
<el-icon><ele-ArrowRight /></el-icon>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
|
|
|
|
|
<el-empty v-if="knowledgeList.length === 0 && !knowledgeLoading" description="暂无知识库,点击上方按钮创建" :image-size="100" />
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 数据集详情页 -->
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="knowledge-detail-view" v-else>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 顶部导航 -->
|
|
|
|
|
|
<div class="detail-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<el-breadcrumb separator=">">
|
|
|
|
|
|
<el-breadcrumb-item>
|
|
|
|
|
|
<span class="back-link" @click="onBackToList">知识库</span>
|
|
|
|
|
|
</el-breadcrumb-item>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-breadcrumb-item>{{ currentknowledge.name }}</el-breadcrumb-item>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</el-breadcrumb>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<div class="detail-body">
|
|
|
|
|
|
<!-- 左侧信息面板 -->
|
|
|
|
|
|
<div class="info-sidebar">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="knowledge-profile">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<div class="profile-icon">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<span class="icon-text">{{ currentknowledge.name?.charAt(0)?.toUpperCase() || 'D' }}</span>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="profile-info">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="profile-name">{{ currentknowledge.name }}</div>
|
2026-04-13 15:16:08 +08:00
|
|
|
|
<div class="profile-meta">{{ currentknowledge.documentCount || 0 }} 个文件 · {{ formatFileSize(currentknowledge.totalSize || 0) }}</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="profile-time">创建于 {{ currentknowledge.createdAt }}</div>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 功能菜单 -->
|
|
|
|
|
|
<div class="func-menu">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="menu-item" :class="{ active: activeMenu === 'files' }" @click="activeMenu = 'files'">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-icon><ele-Document /></el-icon>
|
|
|
|
|
|
<span>文件列表</span>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="menu-item" :class="{ active: activeMenu === 'search' }" @click="activeMenu = 'search'">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-icon><ele-Search /></el-icon>
|
|
|
|
|
|
<span>检索测试</span>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="menu-item" :class="{ active: activeMenu === 'logs' }" @click="activeMenu = 'logs'">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-icon><ele-List /></el-icon>
|
|
|
|
|
|
<span>日志</span>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<div class="menu-item" :class="{ active: activeMenu === 'settings' }" @click="activeMenu = 'settings'">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-icon><ele-Setting /></el-icon>
|
|
|
|
|
|
<span>配置</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 右侧内容区 -->
|
|
|
|
|
|
<div class="main-content">
|
|
|
|
|
|
<!-- 文件列表 -->
|
|
|
|
|
|
<template v-if="activeMenu === 'files'">
|
|
|
|
|
|
<div class="content-header">
|
|
|
|
|
|
<div class="header-title">
|
|
|
|
|
|
<h3>文件列表</h3>
|
|
|
|
|
|
<span class="subtitle">解析成功后才能问答哦。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header-actions">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-input v-model="searchKeyword" placeholder="搜索文件" clearable style="width: 200px" @keyup.enter="getFileList">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<el-icon><ele-Search /></el-icon>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" v-debounce @click="onUploadFile">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-icon><ele-Plus /></el-icon>
|
|
|
|
|
|
新增文件
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="file-table" v-loading="fileLoading">
|
|
|
|
|
|
<el-table :data="fileList" style="width: 100%" row-key="id" border>
|
2026-04-11 16:11:00 +08:00
|
|
|
|
<el-table-column prop="title" label="名称" min-width="200">
|
2026-04-13 15:16:08 +08:00
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="file-link" @click="onViewDocumentDetail(row)" style="cursor: pointer; color: #409eff">{{ row.title }}</span>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-table-column prop="chunkCount" label="分块数" width="80" align="center" />
|
|
|
|
|
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
2026-04-13 15:16:08 +08:00
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-switch v-model="row.statusEnabled" inline-prompt active-text="启" inactive-text="停" @change="onFileStatusChange(row)" />
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-table-column prop="vectorStatus" label="向量化" width="100" align="center">
|
2026-04-13 15:16:08 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-tag :type="getVectorStatusType(row.vectorStatus)" size="small">
|
|
|
|
|
|
{{ getVectorStatusText(row.vectorStatus) }}
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-table-column prop="createdAt" label="上传日期" width="180" />
|
2026-04-11 16:11:00 +08:00
|
|
|
|
<el-table-column label="动作" width="180" align="center">
|
2026-04-13 15:16:08 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button text size="small" v-debounce @click="onPreviewFile(row)">预览</el-button>
|
|
|
|
|
|
<el-button v-if="row.vectorStatus === 1" text size="small" type="primary" v-debounce @click="onGenerateVector(row)"
|
|
|
|
|
|
>生成向量</el-button
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-button v-else text size="small" type="primary" v-debounce @click="onViewTaskList(row)">查看任务</el-button>
|
|
|
|
|
|
<el-button text size="small" type="danger" v-debounce @click="onDeleteFile(row)">删除</el-button>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-empty v-if="fileList.length === 0 && !fileLoading" description="暂无文件,点击上方按钮上传" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 检索测试 -->
|
|
|
|
|
|
<template v-if="activeMenu === 'search'">
|
|
|
|
|
|
<div class="panel-card">
|
|
|
|
|
|
<h3>检索测试</h3>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-input v-model="searchQuery" type="textarea" :rows="3" placeholder="输入问题进行检索测试..." />
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" class="mt15" v-debounce @click="onSearchTest">测试检索</el-button>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<div class="search-results mt15" v-if="searchResults.length > 0">
|
|
|
|
|
|
<h4>检索结果</h4>
|
|
|
|
|
|
<div class="result-item" v-for="(item, index) in searchResults" :key="index">
|
|
|
|
|
|
<div class="result-score">相似度: {{ (item.score * 100).toFixed(1) }}%</div>
|
|
|
|
|
|
<div class="result-content">{{ item.content }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 日志 -->
|
|
|
|
|
|
<template v-if="activeMenu === 'logs'">
|
|
|
|
|
|
<div class="panel-card">
|
|
|
|
|
|
<h3>操作日志</h3>
|
|
|
|
|
|
<el-timeline>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-timeline-item v-for="(log, index) in logList" :key="index" :timestamp="log.time" placement="top">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<span>{{ log.content }}</span>
|
|
|
|
|
|
</el-timeline-item>
|
|
|
|
|
|
</el-timeline>
|
|
|
|
|
|
<el-empty v-if="logList.length === 0" description="暂无日志" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 配置 -->
|
|
|
|
|
|
<template v-if="activeMenu === 'settings'">
|
|
|
|
|
|
<div class="panel-card">
|
|
|
|
|
|
<h3>数据集配置</h3>
|
|
|
|
|
|
<el-form label-width="120px" style="max-width: 500px">
|
|
|
|
|
|
<el-form-item label="数据集名称">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-input v-model="currentknowledge.name" disabled />
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="向量模型">
|
|
|
|
|
|
<el-select v-model="settingsForm.embeddingModel" style="width: 100%">
|
|
|
|
|
|
<el-option label="text-embedding-ada-002" value="text-embedding-ada-002" />
|
|
|
|
|
|
<el-option label="bge-large-zh" value="bge-large-zh" />
|
|
|
|
|
|
<el-option label="m3e-base" value="m3e-base" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="分段长度">
|
|
|
|
|
|
<el-input-number v-model="settingsForm.chunkSize" :min="100" :max="2000" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="重叠长度">
|
|
|
|
|
|
<el-input-number v-model="settingsForm.chunkOverlap" :min="0" :max="500" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" v-debounce @click="onSaveSettings">保存配置</el-button>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 新增/编辑数据集弹窗 -->
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-dialog :title="knowledgeForm.id ? '编辑知识库' : '新建知识库'" v-model="showknowledgeDialog" width="500px" :close-on-click-modal="false">
|
|
|
|
|
|
<el-form ref="knowledgeFormRef" :model="knowledgeForm" :rules="knowledgeRules" label-width="100px">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-form-item label="名称" prop="name">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-input v-model="knowledgeForm.name" placeholder="请输入知识库名称" />
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="描述" prop="description">
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-input v-model="knowledgeForm.description" type="textarea" :rows="3" placeholder="请输入描述" />
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-button @click="showknowledgeDialog = false">取消</el-button>
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" v-debounce @click="onSaveknowledge" :loading="knowledgeSaving">确定</el-button>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<!-- 上传文件弹窗 -->
|
2026-03-30 17:35:05 +08:00
|
|
|
|
<el-dialog title="上传文件" v-model="showUploadDialog" width="600px" :close-on-click-modal="false">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="uploadRef"
|
|
|
|
|
|
class="upload-area"
|
|
|
|
|
|
drag
|
|
|
|
|
|
multiple
|
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
|
:file-list="uploadFileList"
|
|
|
|
|
|
:on-change="onUploadChange"
|
|
|
|
|
|
:on-remove="onUploadRemove"
|
|
|
|
|
|
accept=".pdf,.docx,.doc,.txt,.md,.html,.csv"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon class="el-icon--upload"><ele-UploadFilled /></el-icon>
|
|
|
|
|
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
|
|
|
|
|
<template #tip>
|
|
|
|
|
|
<div class="el-upload__tip">支持 PDF、Word、TXT、Markdown、HTML、CSV 格式</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showUploadDialog = false">取消</el-button>
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" v-debounce @click="onConfirmUpload" :loading="uploading" :disabled="uploadFileList.length === 0">
|
2026-02-02 17:47:11 +08:00
|
|
|
|
上传 ({{ uploadFileList.length }} 个文件)
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-04 15:19:29 +08:00
|
|
|
|
<!-- 文档详情弹窗 -->
|
|
|
|
|
|
<DocumentDetailDialog
|
|
|
|
|
|
v-model="showDocumentDetailDialog"
|
2026-03-30 17:35:05 +08:00
|
|
|
|
:knowledgeId="currentknowledge?.id || ''"
|
|
|
|
|
|
:knowledgeName="currentknowledge?.name || ''"
|
2026-02-04 15:19:29 +08:00
|
|
|
|
:document="currentDocument"
|
|
|
|
|
|
/>
|
2026-04-16 14:49:33 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 模型配置弹窗 -->
|
|
|
|
|
|
<el-dialog title="模型配置" v-model="showModelConfigDialog" width="1000px" :close-on-click-modal="false">
|
|
|
|
|
|
<div class="model-config-list" v-loading="modelConfigLoading">
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" style="margin-bottom: 16px" v-debounce @click="onCreateModelConfig">
|
2026-04-16 14:49:33 +08:00
|
|
|
|
<el-icon><ele-Plus /></el-icon>
|
|
|
|
|
|
创建模型配置
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-table :data="modelConfigList" style="width: 100%" border>
|
|
|
|
|
|
<el-table-column prop="modelName" label="模型名称" min-width="120" />
|
|
|
|
|
|
<el-table-column prop="modelType" label="模型类型" min-width="100" />
|
|
|
|
|
|
<el-table-column prop="modelDesc" label="模型描述" min-width="150" />
|
|
|
|
|
|
<el-table-column prop="configType" label="配置类型" min-width="100" />
|
|
|
|
|
|
<el-table-column prop="createTime" label="创建时间" min-width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ formatDateTime(row.createTime) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="updateTime" label="修改时间" min-width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ formatDateTime(row.updateTime) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="80" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button text size="small" type="primary" v-debounce @click="onEditModelConfig(row)">编辑</el-button>
|
2026-04-16 14:49:33 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-empty v-if="modelConfigList.length === 0 && !modelConfigLoading" description="暂无模型配置" :image-size="60" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showModelConfigDialog = false">关闭</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<!-- 任务列表弹窗 -->
|
|
|
|
|
|
<el-dialog title="任务列表" v-model="showTaskListDialog" width="900px" :close-on-click-modal="false">
|
|
|
|
|
|
<el-table :data="taskList" style="width: 100%" border v-loading="taskListLoading">
|
|
|
|
|
|
<el-table-column prop="taskType" label="任务类型" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ getTaskTypeText(row.taskType) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="status" label="任务状态" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag :type="getTaskStatusType(row.status)" size="small">
|
|
|
|
|
|
{{ getTaskStatusText(row.status) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="remark" label="备注" min-width="200" />
|
|
|
|
|
|
<el-table-column prop="startTime" label="开始时间" width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ row.startTime ? formatDateTime(row.startTime) : '-' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="endTime" label="结束时间" width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ row.endTime ? formatDateTime(row.endTime) : '-' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="120" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="row.status === 'FAILED' || row.status === 'COMPLETED'"
|
|
|
|
|
|
text
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
v-debounce
|
|
|
|
|
|
@click="onReexecuteTask(row)"
|
|
|
|
|
|
>重新执行</el-button
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-button v-else text size="small" type="info" disabled>执行中</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-empty v-if="taskList.length === 0 && !taskListLoading" description="暂无任务" :image-size="60" />
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showTaskListDialog = false">关闭</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
2026-04-16 14:49:33 +08:00
|
|
|
|
<!-- 创建/编辑模型配置弹窗 -->
|
|
|
|
|
|
<el-dialog :title="isEditMode ? '编辑模型配置' : '创建模型配置'" v-model="showCreateModelDialog" width="600px" :close-on-click-modal="false">
|
|
|
|
|
|
<div v-loading="modelEnumsLoading || modelFormLoading">
|
|
|
|
|
|
<el-form :model="modelFormData" label-width="100px">
|
|
|
|
|
|
<!-- 模型类型选择 -->
|
|
|
|
|
|
<el-form-item label="模型类型" required>
|
|
|
|
|
|
<el-select v-model="selectedModelType" style="width: 100%" @change="onModelTypeChange">
|
|
|
|
|
|
<el-option v-for="item in modelEnums" :key="item.key" :label="item.value" :value="item.key" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 配置类型选择 -->
|
|
|
|
|
|
<el-form-item label="配置类型" required>
|
|
|
|
|
|
<el-select v-model="selectedConfigType" style="width: 100%" @change="onConfigTypeChange" :disabled="!selectedModelType">
|
|
|
|
|
|
<el-option v-for="item in getConfigTypes()" :key="item.key" :label="item.value" :value="item.key" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 动态表单字段 -->
|
|
|
|
|
|
<template v-if="modelFormFields.length > 0">
|
|
|
|
|
|
<el-form-item v-for="(field, index) in modelFormFields" :key="field.name || index" :label="field.label" :required="field.required">
|
|
|
|
|
|
<template v-if="field.type === 'textarea'">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="modelFormData[field.name]"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="3"
|
|
|
|
|
|
:placeholder="field.placeholder"
|
|
|
|
|
|
:disabled="field.disabled"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="field.type === 'switch'">
|
|
|
|
|
|
<el-switch v-model="modelFormData[field.name]" :disabled="field.disabled" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<el-input v-model="modelFormData[field.name]" :placeholder="field.placeholder" :disabled="field.disabled" style="width: 100%" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 无表单字段提示 -->
|
|
|
|
|
|
<el-empty v-else description="暂无表单字段" :image-size="60" />
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showCreateModelDialog = false">取消</el-button>
|
2026-04-20 10:20:45 +08:00
|
|
|
|
<el-button type="primary" v-debounce @click="onSaveModelConfig" :disabled="!selectedModelType || !selectedConfigType"> 保存 </el-button>
|
2026-04-16 14:49:33 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-02-02 17:47:11 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'knowledge',
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, reactive, onMounted } from 'vue';
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
|
|
import type { FormInstance, FormRules, UploadFile } from 'element-plus';
|
2026-02-04 15:19:29 +08:00
|
|
|
|
import DocumentDetailDialog from './component/documentDetailDialog.vue';
|
2026-03-30 17:35:05 +08:00
|
|
|
|
import { listknowledges, createknowledge, updateknowledge, deleteknowledge } from '/@/api/knowledge/dataset';
|
2026-04-20 10:20:45 +08:00
|
|
|
|
import {
|
|
|
|
|
|
listDocuments,
|
|
|
|
|
|
uploadFile,
|
|
|
|
|
|
createDocument,
|
|
|
|
|
|
deleteDocument,
|
|
|
|
|
|
updateDocument,
|
|
|
|
|
|
generateVector,
|
|
|
|
|
|
getDocument,
|
|
|
|
|
|
listTasks,
|
|
|
|
|
|
reexecuteTask,
|
|
|
|
|
|
} from '/@/api/knowledge/document';
|
2026-04-16 14:49:33 +08:00
|
|
|
|
import { listModelConfigs, createModelConfig, updateModelConfig, getModelConfig, getAllModelEnums, getModelFormField } from '/@/api/knowledge/model';
|
2026-02-03 15:43:16 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 数据集相关
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const knowledgeLoading = ref(false);
|
|
|
|
|
|
const knowledgeList = ref<any[]>([]);
|
|
|
|
|
|
const currentknowledge = ref<any>(null);
|
|
|
|
|
|
const showknowledgeDialog = ref(false);
|
|
|
|
|
|
const knowledgeSaving = ref(false);
|
2026-04-16 14:49:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 模型配置相关
|
|
|
|
|
|
const showModelConfigDialog = ref(false);
|
|
|
|
|
|
const modelConfigList = ref<any[]>([]);
|
|
|
|
|
|
const modelConfigLoading = ref(false);
|
|
|
|
|
|
|
2026-04-20 10:20:45 +08:00
|
|
|
|
// 任务列表相关
|
|
|
|
|
|
const showTaskListDialog = ref(false);
|
|
|
|
|
|
const taskList = ref<any[]>([]);
|
|
|
|
|
|
const taskListLoading = ref(false);
|
|
|
|
|
|
|
2026-04-16 14:49:33 +08:00
|
|
|
|
// 创建模型配置相关
|
|
|
|
|
|
const showCreateModelDialog = ref(false);
|
|
|
|
|
|
const modelEnums = ref<any[]>([]);
|
|
|
|
|
|
const selectedModelType = ref('');
|
|
|
|
|
|
const selectedConfigType = ref('');
|
|
|
|
|
|
const modelFormFields = ref<any[]>([]);
|
|
|
|
|
|
const modelFormData = ref<any>({});
|
|
|
|
|
|
const modelFormLoading = ref(false);
|
|
|
|
|
|
const modelEnumsLoading = ref(false);
|
|
|
|
|
|
const isEditMode = ref(false);
|
|
|
|
|
|
const currentModelId = ref<number | null>(null);
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const knowledgeFormRef = ref<FormInstance>();
|
|
|
|
|
|
const knowledgeForm = reactive({
|
2026-02-02 17:47:11 +08:00
|
|
|
|
id: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
});
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const knowledgeRules = reactive<FormRules>({
|
2026-02-02 17:47:11 +08:00
|
|
|
|
name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-30 17:35:05 +08:00
|
|
|
|
// 文件列表(含OSS上传结果)
|
|
|
|
|
|
interface UploadFileItem {
|
|
|
|
|
|
file: UploadFile;
|
|
|
|
|
|
filePath: string;
|
|
|
|
|
|
fileSize: number;
|
|
|
|
|
|
fileFormat: string;
|
|
|
|
|
|
fileName: string;
|
|
|
|
|
|
uploading: boolean;
|
|
|
|
|
|
error: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
const uploadFileItems = ref<UploadFileItem[]>([]);
|
|
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 文件相关
|
|
|
|
|
|
const fileLoading = ref(false);
|
|
|
|
|
|
const fileList = ref<any[]>([]);
|
|
|
|
|
|
const searchKeyword = ref('');
|
|
|
|
|
|
const showUploadDialog = ref(false);
|
|
|
|
|
|
const uploadFileList = ref<UploadFile[]>([]);
|
|
|
|
|
|
const uploading = ref(false);
|
|
|
|
|
|
|
2026-02-04 15:19:29 +08:00
|
|
|
|
// 文档详情弹窗
|
|
|
|
|
|
const showDocumentDetailDialog = ref(false);
|
|
|
|
|
|
const currentDocument = ref<any>(null);
|
|
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 菜单相关
|
|
|
|
|
|
const activeMenu = ref('files');
|
|
|
|
|
|
|
|
|
|
|
|
// 检索测试
|
|
|
|
|
|
const searchQuery = ref('');
|
|
|
|
|
|
const searchResults = ref<any[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 日志
|
|
|
|
|
|
const logList = ref<any[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 配置
|
|
|
|
|
|
const settingsForm = reactive({
|
|
|
|
|
|
embeddingModel: 'text-embedding-ada-002',
|
|
|
|
|
|
chunkSize: 500,
|
|
|
|
|
|
chunkOverlap: 50,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化文件大小
|
|
|
|
|
|
const formatFileSize = (size: number) => {
|
|
|
|
|
|
if (!size) return '0 B';
|
|
|
|
|
|
if (size < 1024) return size + ' B';
|
|
|
|
|
|
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
|
|
|
|
|
|
return (size / 1024 / 1024).toFixed(1) + ' MB';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取文件图标颜色
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const _getFileIconColor = (fileType: string) => {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
const colors: Record<string, string> = {
|
|
|
|
|
|
pdf: '#f56c6c',
|
|
|
|
|
|
docx: '#409eff',
|
|
|
|
|
|
doc: '#409eff',
|
|
|
|
|
|
txt: '#909399',
|
|
|
|
|
|
md: '#67c23a',
|
|
|
|
|
|
html: '#e6a23c',
|
|
|
|
|
|
csv: '#67c23a',
|
|
|
|
|
|
};
|
|
|
|
|
|
return colors[fileType] || '#909399';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取解析状态类型
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const _getParseStatusType = (status: string) => {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
const types: Record<string, string> = {
|
|
|
|
|
|
general: 'success',
|
|
|
|
|
|
pending: 'warning',
|
|
|
|
|
|
failed: 'danger',
|
|
|
|
|
|
};
|
|
|
|
|
|
return types[status] || 'info';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取数据集列表
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const getknowledgeList = async () => {
|
|
|
|
|
|
knowledgeLoading.value = true;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
try {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const response = await listknowledges({
|
2026-03-24 18:00:17 +08:00
|
|
|
|
pageNum: 1,
|
2026-03-30 17:35:05 +08:00
|
|
|
|
pageSize: 100,
|
2026-03-24 18:00:17 +08:00
|
|
|
|
});
|
2026-03-30 17:35:05 +08:00
|
|
|
|
knowledgeList.value = response.data.list || [];
|
2026-03-24 18:00:17 +08:00
|
|
|
|
} catch (_error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-02-02 17:47:11 +08:00
|
|
|
|
} finally {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
knowledgeLoading.value = false;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 选择数据集
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const onSelectknowledge = (item: any) => {
|
|
|
|
|
|
currentknowledge.value = item;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
activeMenu.value = 'files';
|
|
|
|
|
|
getFileList();
|
|
|
|
|
|
getLogList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 新增数据集
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const onAddknowledge = () => {
|
|
|
|
|
|
knowledgeForm.id = '';
|
|
|
|
|
|
knowledgeForm.name = '';
|
|
|
|
|
|
knowledgeForm.description = '';
|
|
|
|
|
|
showknowledgeDialog.value = true;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 返回列表
|
|
|
|
|
|
const onBackToList = () => {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
currentknowledge.value = null;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 右键菜单
|
|
|
|
|
|
const onCardContextMenu = (event: MouseEvent, item: any) => {
|
|
|
|
|
|
// 可以在这里实现右键菜单,暂时用悬停按钮代替
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 重命名数据集
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const onRenameknowledge = (item: any) => {
|
|
|
|
|
|
knowledgeForm.id = item.id;
|
|
|
|
|
|
knowledgeForm.name = item.name;
|
|
|
|
|
|
knowledgeForm.description = item.description || '';
|
|
|
|
|
|
showknowledgeDialog.value = true;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 删除数据集
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const onDeleteknowledge = (item: any) => {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
ElMessageBox.confirm(`确定要删除知识库【${item.name}】吗?`, '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
2026-03-30 17:35:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
.then(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteknowledge(item.id);
|
|
|
|
|
|
ElMessage.success('删除成功');
|
|
|
|
|
|
if (currentknowledge.value?.id === item.id) {
|
|
|
|
|
|
currentknowledge.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
getknowledgeList();
|
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
|
ElMessage.error('删除失败,请重试');
|
2026-03-24 18:00:17 +08:00
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存数据集
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const onSaveknowledge = async () => {
|
|
|
|
|
|
const form = knowledgeFormRef.value;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
if (!form) return;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
form.validate(async (valid: boolean) => {
|
|
|
|
|
|
if (valid) {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
knowledgeSaving.value = true;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
try {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
if (knowledgeForm.id) {
|
2026-03-24 18:00:17 +08:00
|
|
|
|
// 更新知识库
|
2026-03-30 17:35:05 +08:00
|
|
|
|
await updateknowledge({
|
|
|
|
|
|
id: knowledgeForm.id,
|
|
|
|
|
|
name: knowledgeForm.name,
|
|
|
|
|
|
description: knowledgeForm.description,
|
2026-03-24 18:00:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 创建知识库
|
2026-03-30 17:35:05 +08:00
|
|
|
|
await createknowledge({
|
|
|
|
|
|
name: knowledgeForm.name,
|
|
|
|
|
|
description: knowledgeForm.description,
|
2026-03-24 18:00:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
|
|
|
|
|
ElMessage.success(knowledgeForm.id ? '保存成功' : '创建成功');
|
|
|
|
|
|
showknowledgeDialog.value = false;
|
|
|
|
|
|
getknowledgeList();
|
2026-03-24 18:00:17 +08:00
|
|
|
|
} catch (_error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-02-02 17:47:11 +08:00
|
|
|
|
} finally {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
knowledgeSaving.value = false;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取文件列表
|
|
|
|
|
|
const getFileList = async () => {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
if (!currentknowledge.value) return;
|
|
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
fileLoading.value = true;
|
|
|
|
|
|
try {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const response = await listDocuments({
|
|
|
|
|
|
datasetId: currentknowledge.value.id,
|
|
|
|
|
|
...(searchKeyword.value ? { keyword: searchKeyword.value } : {}),
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 100,
|
|
|
|
|
|
});
|
2026-04-11 16:11:00 +08:00
|
|
|
|
fileList.value = (response.data?.list || []).map((item: any) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
statusEnabled: item.status === 1,
|
|
|
|
|
|
}));
|
2026-03-24 18:00:17 +08:00
|
|
|
|
} catch (_error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-02-02 17:47:11 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
fileLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 上传文件
|
|
|
|
|
|
const onUploadFile = () => {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
uploadFileItems.value = [];
|
2026-02-02 17:47:11 +08:00
|
|
|
|
uploadFileList.value = [];
|
|
|
|
|
|
showUploadDialog.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-30 17:35:05 +08:00
|
|
|
|
// 选择文件时立即上传到OSS
|
|
|
|
|
|
const onUploadChange = async (file: UploadFile, files: UploadFile[]) => {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
uploadFileList.value = files;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
// 找出新增的文件(还没有对应的 item)
|
|
|
|
|
|
const exists = uploadFileItems.value.some((i) => i.file.uid === file.uid);
|
|
|
|
|
|
if (exists || !file.raw) return;
|
|
|
|
|
|
const item: UploadFileItem = {
|
|
|
|
|
|
file,
|
|
|
|
|
|
filePath: '',
|
|
|
|
|
|
fileSize: 0,
|
|
|
|
|
|
fileFormat: '',
|
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
|
uploading: true,
|
|
|
|
|
|
error: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
uploadFileItems.value.push(item);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ossRes = await uploadFile(file.raw as File);
|
|
|
|
|
|
item.filePath = ossRes.data?.fileURL || '';
|
|
|
|
|
|
item.fileSize = ossRes.data?.fileSize || file.size || 0;
|
|
|
|
|
|
item.fileFormat = ossRes.data?.fileFormat || file.name.split('.').pop() || '';
|
|
|
|
|
|
item.fileName = ossRes.data?.fileName || file.name;
|
|
|
|
|
|
item.uploading = false;
|
|
|
|
|
|
} catch (_e) {
|
|
|
|
|
|
item.uploading = false;
|
|
|
|
|
|
item.error = true;
|
|
|
|
|
|
ElMessage.error(`${file.name} 上传失败`);
|
|
|
|
|
|
}
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 移除上传文件
|
|
|
|
|
|
const onUploadRemove = (file: UploadFile, files: UploadFile[]) => {
|
|
|
|
|
|
uploadFileList.value = files;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
uploadFileItems.value = uploadFileItems.value.filter((i) => i.file.uid !== file.uid);
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-30 17:35:05 +08:00
|
|
|
|
// 确认上传:所有文件已上传OSS,直接创建文档
|
2026-02-02 17:47:11 +08:00
|
|
|
|
const onConfirmUpload = async () => {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const readyItems = uploadFileItems.value.filter((i) => !i.uploading && !i.error && i.filePath);
|
|
|
|
|
|
if (readyItems.length === 0) {
|
|
|
|
|
|
ElMessage.warning('请等待文件上传完成或移除上传失败的文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-02 17:47:11 +08:00
|
|
|
|
uploading.value = true;
|
|
|
|
|
|
try {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
for (const item of readyItems) {
|
|
|
|
|
|
const ext = item.file.name.split('.').pop() || '';
|
|
|
|
|
|
await createDocument({
|
|
|
|
|
|
datasetId: currentknowledge.value.id,
|
|
|
|
|
|
filePath: item.filePath,
|
|
|
|
|
|
fileSize: item.fileSize || item.file.size || 0,
|
|
|
|
|
|
format: item.fileFormat || ext,
|
|
|
|
|
|
title: item.fileName || item.file.name,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
ElMessage.success(`成功创建 ${readyItems.length} 个文件`);
|
2026-02-02 17:47:11 +08:00
|
|
|
|
showUploadDialog.value = false;
|
|
|
|
|
|
getFileList();
|
2026-03-24 18:00:17 +08:00
|
|
|
|
} catch (_error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-02-02 17:47:11 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
uploading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 文件状态变化
|
2026-04-11 16:11:00 +08:00
|
|
|
|
const onFileStatusChange = async (row: any) => {
|
|
|
|
|
|
const newStatus = row.statusEnabled ? 1 : 0;
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用后端API来更新文件状态
|
|
|
|
|
|
await updateDocument({
|
|
|
|
|
|
id: row.id,
|
|
|
|
|
|
status: newStatus,
|
|
|
|
|
|
});
|
|
|
|
|
|
ElMessage.success(row.statusEnabled ? '已启用' : '已禁用');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 失败时恢复原状态
|
|
|
|
|
|
row.statusEnabled = !row.statusEnabled;
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-11 16:11:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 生成向量
|
|
|
|
|
|
const onGenerateVector = async (row: any) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用后端API来生成向量,传递id和datasetId
|
|
|
|
|
|
await generateVector(row.id, currentknowledge.value.id);
|
|
|
|
|
|
ElMessage.success('生成向量任务已提交');
|
|
|
|
|
|
// 模拟更新状态
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
getFileList();
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-11 16:11:00 +08:00
|
|
|
|
}
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 15:43:16 +08:00
|
|
|
|
// 查看文档详情
|
2026-04-11 16:11:00 +08:00
|
|
|
|
const onViewDocumentDetail = async (row: any) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用getDocument接口获取最新的文件详情
|
|
|
|
|
|
const response = await getDocument(row.id);
|
|
|
|
|
|
currentDocument.value = response.data;
|
|
|
|
|
|
showDocumentDetailDialog.value = true;
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-11 16:11:00 +08:00
|
|
|
|
}
|
2026-02-03 15:43:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 预览文件
|
|
|
|
|
|
const onPreviewFile = (row: any) => {
|
2026-02-03 15:43:16 +08:00
|
|
|
|
onViewDocumentDetail(row);
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 下载文件
|
2026-03-30 17:35:05 +08:00
|
|
|
|
const _onDownloadFile = (row: any) => {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
ElMessage.info(`下载文件: ${row.name}`);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 删除文件
|
|
|
|
|
|
const onDeleteFile = (row: any) => {
|
|
|
|
|
|
ElMessageBox.confirm(`确定要删除文件【${row.name}】吗?`, '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
2026-03-30 17:35:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
.then(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteDocument(row.id);
|
|
|
|
|
|
ElMessage.success('删除成功');
|
|
|
|
|
|
getFileList();
|
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
|
ElMessage.error('删除失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 检索测试
|
|
|
|
|
|
const onSearchTest = () => {
|
|
|
|
|
|
if (!searchQuery.value.trim()) {
|
|
|
|
|
|
ElMessage.warning('请输入检索内容');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 模拟检索结果
|
|
|
|
|
|
searchResults.value = [
|
|
|
|
|
|
{ score: 0.92, content: '这是检索到的第一条相关内容...' },
|
|
|
|
|
|
{ score: 0.85, content: '这是检索到的第二条相关内容...' },
|
|
|
|
|
|
{ score: 0.78, content: '这是检索到的第三条相关内容...' },
|
|
|
|
|
|
];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取日志列表
|
|
|
|
|
|
const getLogList = () => {
|
|
|
|
|
|
logList.value = [
|
|
|
|
|
|
{ time: '2026-01-21 16:53:32', content: '上传文件 456_product(1).txt' },
|
|
|
|
|
|
{ time: '2026-01-21 16:53:26', content: '上传文件 123_speech(1).txt' },
|
|
|
|
|
|
{ time: '2026-01-21 14:39:41', content: '上传文件 123_product.txt' },
|
2026-03-30 17:35:05 +08:00
|
|
|
|
{ time: '2026-01-17 10:00:00', content: '创建知识库 knowledge_tenant_1' },
|
2026-02-02 17:47:11 +08:00
|
|
|
|
];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存配置
|
2026-04-16 14:49:33 +08:00
|
|
|
|
const onSaveSettings = async () => {
|
|
|
|
|
|
ElMessage.success('保存成功');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 打开模型配置弹窗
|
|
|
|
|
|
const onOpenModelConfig = async () => {
|
|
|
|
|
|
await getModelConfigList();
|
|
|
|
|
|
showModelConfigDialog.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 打开创建模型配置弹窗
|
|
|
|
|
|
const onCreateModelConfig = async () => {
|
|
|
|
|
|
// 重置状态
|
|
|
|
|
|
selectedModelType.value = '';
|
|
|
|
|
|
selectedConfigType.value = '';
|
|
|
|
|
|
modelFormFields.value = [];
|
|
|
|
|
|
modelFormData.value = {};
|
|
|
|
|
|
isEditMode.value = false;
|
|
|
|
|
|
currentModelId.value = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取模型类型和配置类型枚举
|
|
|
|
|
|
await getModelEnums();
|
|
|
|
|
|
|
|
|
|
|
|
// 打开创建弹窗
|
|
|
|
|
|
showCreateModelDialog.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 编辑模型配置
|
|
|
|
|
|
const onEditModelConfig = async (row: any) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 重置状态
|
|
|
|
|
|
selectedModelType.value = '';
|
|
|
|
|
|
selectedConfigType.value = '';
|
|
|
|
|
|
modelFormFields.value = [];
|
|
|
|
|
|
modelFormData.value = {};
|
|
|
|
|
|
isEditMode.value = true;
|
|
|
|
|
|
currentModelId.value = row.id;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取模型类型和配置类型枚举
|
|
|
|
|
|
await getModelEnums();
|
|
|
|
|
|
|
|
|
|
|
|
// 调用获取详情接口
|
|
|
|
|
|
const response = await getModelConfig(row.id, row.modelType);
|
|
|
|
|
|
const modelData = response.data;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置模型类型和配置类型
|
|
|
|
|
|
selectedModelType.value = modelData.modelType;
|
|
|
|
|
|
selectedConfigType.value = modelData.configType;
|
|
|
|
|
|
|
|
|
|
|
|
// 填充表单数据
|
|
|
|
|
|
modelFormData.value = {
|
|
|
|
|
|
modelName: modelData.modelName,
|
|
|
|
|
|
modelDesc: modelData.modelDesc,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 将configContent中的数据添加到表单数据中
|
|
|
|
|
|
if (modelData.configContent) {
|
|
|
|
|
|
Object.keys(modelData.configContent).forEach((key) => {
|
|
|
|
|
|
modelFormData.value[key] = modelData.configContent[key];
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取动态表单字段
|
|
|
|
|
|
await getModelFormFields();
|
|
|
|
|
|
|
|
|
|
|
|
// 打开弹窗
|
|
|
|
|
|
showCreateModelDialog.value = true;
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-16 14:49:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取模型类型和配置类型枚举
|
|
|
|
|
|
const getModelEnums = async () => {
|
|
|
|
|
|
modelEnumsLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await getAllModelEnums();
|
|
|
|
|
|
modelEnums.value = response.data?.options || [];
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-16 14:49:33 +08:00
|
|
|
|
modelEnums.value = [];
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modelEnumsLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 模型类型选择变化
|
|
|
|
|
|
const onModelTypeChange = async () => {
|
|
|
|
|
|
selectedConfigType.value = '';
|
|
|
|
|
|
modelFormFields.value = [];
|
|
|
|
|
|
// 在编辑模式下,只保留模型名称和描述,清空其他字段
|
|
|
|
|
|
if (isEditMode.value) {
|
|
|
|
|
|
const { modelName, modelDesc } = modelFormData.value;
|
|
|
|
|
|
modelFormData.value = {
|
|
|
|
|
|
modelName,
|
|
|
|
|
|
modelDesc,
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 创建模式下清空所有字段
|
|
|
|
|
|
modelFormData.value = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 配置类型选择变化
|
|
|
|
|
|
const onConfigTypeChange = async () => {
|
|
|
|
|
|
if (selectedModelType.value && selectedConfigType.value) {
|
|
|
|
|
|
await getModelFormFields();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取模型表单字段
|
|
|
|
|
|
const getModelFormFields = async () => {
|
|
|
|
|
|
modelFormLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await getModelFormField(selectedModelType.value, selectedConfigType.value);
|
|
|
|
|
|
// 过滤掉模型类型和配置类型字段,避免重复显示
|
|
|
|
|
|
modelFormFields.value = (response.data?.fields || []).filter((field: any) => {
|
|
|
|
|
|
return field.name !== 'modelType' && field.name !== 'configType';
|
|
|
|
|
|
});
|
|
|
|
|
|
// 设置字段的默认值,但保留已有的表单数据
|
|
|
|
|
|
modelFormFields.value.forEach((field: any) => {
|
|
|
|
|
|
if (field.value !== undefined && modelFormData.value[field.name] === undefined) {
|
|
|
|
|
|
modelFormData.value[field.name] = field.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-16 14:49:33 +08:00
|
|
|
|
modelFormFields.value = [];
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modelFormLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存模型配置
|
|
|
|
|
|
const onSaveModelConfig = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 构建请求数据,只传递接口需要的字段
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
modelType: selectedModelType.value,
|
|
|
|
|
|
configType: selectedConfigType.value,
|
|
|
|
|
|
modelName: modelFormData.value.modelName,
|
|
|
|
|
|
modelDesc: modelFormData.value.modelDesc,
|
|
|
|
|
|
configContent: {} as Record<string, any>,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 将动态表单字段(除了modelType、configType、modelName、modelDesc)添加到configContent中,以key-value形式
|
|
|
|
|
|
Object.keys(modelFormData.value).forEach((key) => {
|
|
|
|
|
|
if (!['modelType', 'configType', 'modelName', 'modelDesc'].includes(key)) {
|
|
|
|
|
|
data.configContent[key] = modelFormData.value[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 根据模式调用不同的接口
|
|
|
|
|
|
if (isEditMode.value && currentModelId.value) {
|
|
|
|
|
|
// 编辑模式,调用更新接口
|
|
|
|
|
|
await updateModelConfig({ ...data, id: currentModelId.value });
|
|
|
|
|
|
ElMessage.success('更新模型配置成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 创建模式,调用创建接口
|
|
|
|
|
|
await createModelConfig(data);
|
|
|
|
|
|
ElMessage.success('创建模型配置成功');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭弹窗并刷新列表
|
|
|
|
|
|
showCreateModelDialog.value = false;
|
|
|
|
|
|
getModelConfigList();
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-16 14:49:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 根据选中的模型类型获取配置类型列表
|
|
|
|
|
|
const getConfigTypes = () => {
|
|
|
|
|
|
if (!selectedModelType.value) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const selectedModel = modelEnums.value.find((item: any) => item.key === selectedModelType.value);
|
|
|
|
|
|
return selectedModel?.configTypes || [];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取模型配置列表
|
|
|
|
|
|
const getModelConfigList = async () => {
|
|
|
|
|
|
modelConfigLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await listModelConfigs({
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 100,
|
|
|
|
|
|
});
|
|
|
|
|
|
modelConfigList.value = response.data?.list || [];
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-16 14:49:33 +08:00
|
|
|
|
modelConfigList.value = [];
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modelConfigLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化时间
|
|
|
|
|
|
const formatDateTime = (dateTime: string) => {
|
|
|
|
|
|
if (!dateTime) return '-';
|
|
|
|
|
|
const date = new Date(dateTime);
|
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
|
|
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
|
|
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
2026-02-02 17:47:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-20 10:20:45 +08:00
|
|
|
|
// 获取向量状态文本
|
|
|
|
|
|
const getVectorStatusText = (status: number) => {
|
|
|
|
|
|
const statusMap: Record<number, string> = {
|
|
|
|
|
|
1: '待处理',
|
|
|
|
|
|
2: '处理中',
|
|
|
|
|
|
3: '已完成',
|
|
|
|
|
|
4: '失败',
|
|
|
|
|
|
};
|
|
|
|
|
|
return statusMap[status] || '未知';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取向量状态标签类型
|
|
|
|
|
|
const getVectorStatusType = (status: number) => {
|
|
|
|
|
|
const typeMap: Record<number, any> = {
|
|
|
|
|
|
1: 'warning',
|
|
|
|
|
|
2: 'primary',
|
|
|
|
|
|
3: 'success',
|
|
|
|
|
|
4: 'danger',
|
|
|
|
|
|
};
|
|
|
|
|
|
return typeMap[status] || 'info';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取任务类型文本
|
|
|
|
|
|
const getTaskTypeText = (taskType: string) => {
|
|
|
|
|
|
const typeMap: Record<string, string> = {
|
|
|
|
|
|
EXTRACT_KEYWORDS: '提取关键词',
|
|
|
|
|
|
GENERATE_VECTOR: '生成向量',
|
|
|
|
|
|
FULL_TEXT_SEARCH: '全文检索',
|
|
|
|
|
|
DOC_PARSE: '文档解析',
|
|
|
|
|
|
};
|
|
|
|
|
|
return typeMap[taskType] || taskType;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取任务状态文本
|
|
|
|
|
|
const getTaskStatusText = (status: string) => {
|
|
|
|
|
|
const statusMap: Record<string, string> = {
|
|
|
|
|
|
PENDING: '待执行',
|
|
|
|
|
|
RUNNING: '执行中',
|
|
|
|
|
|
COMPLETED: '已完成',
|
|
|
|
|
|
FAILED: '执行失败',
|
|
|
|
|
|
};
|
|
|
|
|
|
return statusMap[status] || status;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取任务状态标签类型
|
|
|
|
|
|
const getTaskStatusType = (status: string) => {
|
|
|
|
|
|
const typeMap: Record<string, any> = {
|
|
|
|
|
|
PENDING: 'warning',
|
|
|
|
|
|
RUNNING: 'primary',
|
|
|
|
|
|
COMPLETED: 'success',
|
|
|
|
|
|
FAILED: 'danger',
|
|
|
|
|
|
};
|
|
|
|
|
|
return typeMap[status] || 'info';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 查看任务列表
|
|
|
|
|
|
const onViewTaskList = async (row: any) => {
|
|
|
|
|
|
showTaskListDialog.value = true;
|
|
|
|
|
|
await getTaskList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取任务列表
|
|
|
|
|
|
const getTaskList = async () => {
|
|
|
|
|
|
taskListLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await listTasks();
|
|
|
|
|
|
taskList.value = response.data?.list || [];
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-20 10:20:45 +08:00
|
|
|
|
taskList.value = [];
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
taskListLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 重新执行任务
|
|
|
|
|
|
const onReexecuteTask = async (task: any) => {
|
|
|
|
|
|
ElMessageBox.confirm(`确定要重新执行任务【${getTaskTypeText(task.taskType)}】吗?`, '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await reexecuteTask(task.id);
|
|
|
|
|
|
ElMessage.success('重新执行任务成功');
|
|
|
|
|
|
// 重新获取任务列表
|
|
|
|
|
|
await getTaskList();
|
|
|
|
|
|
} catch (error) {
|
2026-05-11 20:01:03 +08:00
|
|
|
|
// 错误已由全局拦截器处理
|
2026-04-20 10:20:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 页面加载
|
|
|
|
|
|
onMounted(() => {
|
2026-03-30 17:35:05 +08:00
|
|
|
|
getknowledgeList();
|
2026-02-02 17:47:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.knowledge-page {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
box-sizing: border-box;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 数据集列表页
|
2026-03-30 17:35:05 +08:00
|
|
|
|
.knowledge-list-view {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.page-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 24px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.header-icon {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.header-title {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
|
|
|
|
|
.knowledge-cards {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 16px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
|
|
|
|
|
.knowledge-card {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
width: 200px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
position: relative;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.card-actions {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.card-icon {
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.icon-text {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.card-info {
|
|
|
|
|
|
.card-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.card-meta {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.card-time {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.card-actions {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 8px;
|
|
|
|
|
|
right: 8px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.see-all-card {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px dashed #dcdfe6;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
transition: all 0.3s;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 数据集详情页
|
2026-03-30 17:35:05 +08:00
|
|
|
|
.knowledge-detail-view {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.detail-header {
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.back-link {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
cursor: pointer;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.detail-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
overflow: hidden;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 左侧信息面板
|
|
|
|
|
|
.info-sidebar {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
border-right: 1px solid #ebeef5;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background: #fafafa;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
|
|
|
|
|
.knowledge-profile {
|
2026-02-02 17:47:11 +08:00
|
|
|
|
padding: 20px 16px;
|
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.profile-icon {
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.icon-text {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.profile-info {
|
|
|
|
|
|
.profile-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.profile-meta {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.profile-time {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.func-menu {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 12px 0;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.menu-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
font-size: 14px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.el-icon {
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
&.active {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
|
border-left: 3px solid #409eff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
// 右侧主内容
|
|
|
|
|
|
.main-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
background: #f5f7fa;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.content-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
margin-bottom: 16px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.header-title {
|
|
|
|
|
|
h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.header-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.file-table {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 16px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.file-name {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-03 15:43:16 +08:00
|
|
|
|
cursor: pointer;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.file-icon {
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-03 15:43:16 +08:00
|
|
|
|
.file-link {
|
|
|
|
|
|
color: #409eff;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-03 15:43:16 +08:00
|
|
|
|
&:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 17:47:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.panel-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 20px;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
h3 {
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
h4 {
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.search-results {
|
|
|
|
|
|
.result-item {
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
2026-03-30 17:35:05 +08:00
|
|
|
|
|
2026-02-02 17:47:11 +08:00
|
|
|
|
.result-score {
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.result-content {
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-area {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
:deep(.el-upload) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
:deep(.el-upload-dragger) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|