2025-04-14 10:11:02 +08:00
|
|
|
|
<script lang="ts" setup>
|
2025-04-19 01:27:37 +08:00
|
|
|
|
import type { SequentialBatchTaskProgress } from "@@/apis/kbs/document"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
import type { FormInstance } from "element-plus"
|
2025-04-14 21:23:11 +08:00
|
|
|
|
import DocumentParseProgress from "@/layouts/components/DocumentParseProgress/index.vue"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
import {
|
|
|
|
|
deleteDocumentApi,
|
|
|
|
|
getDocumentListApi,
|
|
|
|
|
getFileListApi,
|
2025-04-19 01:27:37 +08:00
|
|
|
|
getSequentialBatchParseProgressApi,
|
|
|
|
|
runDocumentParseApi,
|
|
|
|
|
startSequentialBatchParseAsyncApi
|
2025-04-14 10:11:02 +08:00
|
|
|
|
} from "@@/apis/kbs/document"
|
|
|
|
|
import {
|
|
|
|
|
batchDeleteKnowledgeBaseApi,
|
|
|
|
|
createKnowledgeBaseApi,
|
|
|
|
|
deleteKnowledgeBaseApi,
|
2025-04-18 22:34:25 +08:00
|
|
|
|
getKnowledgeBaseListApi,
|
|
|
|
|
getSystemEmbeddingConfigApi,
|
|
|
|
|
setSystemEmbeddingConfigApi
|
2025-04-14 10:11:02 +08:00
|
|
|
|
} from "@@/apis/kbs/knowledgebase"
|
|
|
|
|
import { usePagination } from "@@/composables/usePagination"
|
2025-04-19 01:27:37 +08:00
|
|
|
|
import { CaretRight, Delete, Loading, Plus, Refresh, Search, Setting, View } from "@element-plus/icons-vue"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
import axios from "axios"
|
|
|
|
|
import { ElMessage, ElMessageBox } from "element-plus"
|
2025-04-18 22:34:25 +08:00
|
|
|
|
import { nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, reactive, ref, watch } from "vue"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
import "element-plus/dist/index.css"
|
|
|
|
|
import "element-plus/theme-chalk/el-message-box.css"
|
|
|
|
|
import "element-plus/theme-chalk/el-message.css"
|
|
|
|
|
|
|
|
|
|
defineOptions({
|
|
|
|
|
// 命名当前组件
|
|
|
|
|
name: "KnowledgeBase"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const loading = ref<boolean>(false)
|
|
|
|
|
const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
|
|
|
|
|
const createDialogVisible = ref(false)
|
|
|
|
|
const uploadLoading = ref(false)
|
2025-04-14 21:23:11 +08:00
|
|
|
|
const showParseProgress = ref(false)
|
|
|
|
|
const currentDocId = ref("")
|
2025-04-14 10:11:02 +08:00
|
|
|
|
|
2025-04-15 00:35:33 +08:00
|
|
|
|
// 添加清理函数
|
|
|
|
|
function cleanupResources() {
|
|
|
|
|
// 重置所有状态
|
|
|
|
|
if (multipleSelection.value) {
|
|
|
|
|
multipleSelection.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
documentLoading.value = false
|
|
|
|
|
fileLoading.value = false
|
|
|
|
|
uploadLoading.value = false
|
|
|
|
|
|
|
|
|
|
// 关闭所有对话框
|
|
|
|
|
viewDialogVisible.value = false
|
|
|
|
|
createDialogVisible.value = false
|
|
|
|
|
addDocumentDialogVisible.value = false
|
|
|
|
|
showParseProgress.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在组件停用时清理资源
|
|
|
|
|
onDeactivated(() => {
|
|
|
|
|
cleanupResources()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 在组件卸载前清理资源
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
cleanupResources()
|
|
|
|
|
})
|
|
|
|
|
|
2025-04-14 10:11:02 +08:00
|
|
|
|
// 定义知识库数据类型
|
|
|
|
|
interface KnowledgeBaseData {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
description: string
|
|
|
|
|
doc_num: number
|
|
|
|
|
create_time: number
|
|
|
|
|
create_date: string
|
|
|
|
|
avatar?: string
|
|
|
|
|
language: string
|
|
|
|
|
permission: string
|
|
|
|
|
chunk_num: number
|
|
|
|
|
token_num: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 新建知识库表单
|
|
|
|
|
const knowledgeBaseForm = reactive({
|
|
|
|
|
name: "",
|
|
|
|
|
description: "",
|
|
|
|
|
language: "Chinese",
|
|
|
|
|
permission: "me"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 定义API返回数据的接口
|
|
|
|
|
interface FileListResponse {
|
|
|
|
|
list: any[]
|
|
|
|
|
total: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ApiResponse<T> {
|
|
|
|
|
data: T
|
|
|
|
|
code: number
|
|
|
|
|
message: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ListResponse {
|
|
|
|
|
list: any[]
|
|
|
|
|
total: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表单验证规则
|
|
|
|
|
const knowledgeBaseFormRules = {
|
|
|
|
|
name: [
|
|
|
|
|
{ required: true, message: "请输入知识库名称", trigger: "blur" },
|
|
|
|
|
{ min: 2, max: 50, message: "长度在 2 到 50 个字符", trigger: "blur" }
|
|
|
|
|
],
|
|
|
|
|
description: [
|
|
|
|
|
{ max: 200, message: "描述不能超过200个字符", trigger: "blur" }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const knowledgeBaseFormRef = ref<FormInstance | null>(null)
|
|
|
|
|
|
|
|
|
|
// 查询知识库列表
|
|
|
|
|
const tableData = ref<KnowledgeBaseData[]>([])
|
|
|
|
|
const searchFormRef = ref<FormInstance | null>(null)
|
|
|
|
|
const searchData = reactive({
|
|
|
|
|
name: ""
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 存储多选的表格数据
|
|
|
|
|
const multipleSelection = ref<KnowledgeBaseData[]>([])
|
|
|
|
|
|
|
|
|
|
// 获取知识库列表数据
|
|
|
|
|
function getTableData() {
|
|
|
|
|
loading.value = true
|
|
|
|
|
// 调用获取知识库列表API
|
|
|
|
|
getKnowledgeBaseListApi({
|
|
|
|
|
currentPage: paginationData.currentPage,
|
|
|
|
|
size: paginationData.pageSize,
|
|
|
|
|
name: searchData.name
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
const result = response as ApiResponse<ListResponse>
|
|
|
|
|
paginationData.total = result.data.total
|
|
|
|
|
tableData.value = result.data.list
|
|
|
|
|
// 清空选中数据
|
|
|
|
|
multipleSelection.value = []
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
tableData.value = []
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
loading.value = false
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 搜索处理
|
|
|
|
|
function handleSearch() {
|
|
|
|
|
paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置搜索
|
|
|
|
|
function resetSearch() {
|
|
|
|
|
searchFormRef.value?.resetFields()
|
|
|
|
|
handleSearch()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 打开新建知识库对话框
|
|
|
|
|
function handleCreate() {
|
|
|
|
|
createDialogVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 提交新建知识库
|
|
|
|
|
async function submitCreate() {
|
|
|
|
|
if (!knowledgeBaseFormRef.value) return
|
|
|
|
|
|
|
|
|
|
await knowledgeBaseFormRef.value.validate(async (valid) => {
|
|
|
|
|
if (valid) {
|
|
|
|
|
uploadLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
await createKnowledgeBaseApi(knowledgeBaseForm)
|
|
|
|
|
ElMessage.success("知识库创建成功")
|
|
|
|
|
getTableData()
|
|
|
|
|
createDialogVisible.value = false
|
|
|
|
|
// 重置表单
|
|
|
|
|
knowledgeBaseFormRef.value?.resetFields()
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
let errorMessage = "创建失败"
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
errorMessage += `: ${error.message}`
|
|
|
|
|
}
|
|
|
|
|
ElMessage.error(errorMessage)
|
|
|
|
|
} finally {
|
|
|
|
|
uploadLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查看知识库详情
|
|
|
|
|
const viewDialogVisible = ref(false)
|
2025-04-19 01:27:37 +08:00
|
|
|
|
const batchParsingLoading = ref(false) // 批量解析加载状态
|
2025-04-14 10:11:02 +08:00
|
|
|
|
const currentKnowledgeBase = ref<KnowledgeBaseData | null>(null)
|
|
|
|
|
const documentLoading = ref(false)
|
|
|
|
|
const documentList = ref<any[]>([])
|
|
|
|
|
|
2025-04-19 01:27:37 +08:00
|
|
|
|
// 顺序批量任务轮询相关状态
|
|
|
|
|
const batchPollingInterval = ref<NodeJS.Timeout | null>(null) // 定时器 ID
|
|
|
|
|
const isBatchPolling = ref(false) // 是否正在轮询批量任务
|
|
|
|
|
const batchProgress = ref<SequentialBatchTaskProgress | null>(null) // 存储批量任务进度信息
|
|
|
|
|
// 计算批量解析按钮是否禁用
|
|
|
|
|
const isBatchParseDisabled = computed(() => {
|
|
|
|
|
// 如果正在轮询批量任务,则禁用
|
|
|
|
|
if (isBatchPolling.value) return true
|
|
|
|
|
// 如果文档列表为空,或者所有文档都已完成,则禁用
|
|
|
|
|
if (!documentList.value || documentList.value.length === 0) return true
|
|
|
|
|
return documentList.value.every(doc => doc.status === "3")
|
|
|
|
|
})
|
|
|
|
|
|
2025-04-14 10:11:02 +08:00
|
|
|
|
// 文档列表分页
|
|
|
|
|
const docPaginationData = reactive({
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
total: 0,
|
|
|
|
|
pageSizes: [10, 20, 50, 100],
|
|
|
|
|
layout: "total, sizes, prev, pager, next, jumper"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 处理文档分页变化
|
|
|
|
|
function handleDocCurrentChange(page: number) {
|
|
|
|
|
docPaginationData.currentPage = page
|
|
|
|
|
getDocumentList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleDocSizeChange(size: number) {
|
|
|
|
|
docPaginationData.pageSize = size
|
|
|
|
|
docPaginationData.currentPage = 1
|
|
|
|
|
getDocumentList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取知识库下的文档列表
|
|
|
|
|
function getDocumentList() {
|
|
|
|
|
if (!currentKnowledgeBase.value) return
|
|
|
|
|
|
|
|
|
|
documentLoading.value = true
|
|
|
|
|
getDocumentListApi({
|
|
|
|
|
kb_id: currentKnowledgeBase.value.id,
|
|
|
|
|
currentPage: docPaginationData.currentPage,
|
|
|
|
|
size: docPaginationData.pageSize,
|
|
|
|
|
name: ""
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
const result = response as ApiResponse<ListResponse>
|
|
|
|
|
documentList.value = result.data.list
|
|
|
|
|
docPaginationData.total = result.data.total
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
ElMessage.error(`获取文档列表失败: ${error?.message || "未知错误"}`)
|
|
|
|
|
documentList.value = []
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
documentLoading.value = false
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 修改handleView方法
|
|
|
|
|
function handleView(row: KnowledgeBaseData) {
|
|
|
|
|
currentKnowledgeBase.value = row
|
|
|
|
|
viewDialogVisible.value = true
|
|
|
|
|
// 重置文档分页
|
|
|
|
|
docPaginationData.currentPage = 1
|
2025-04-19 01:27:37 +08:00
|
|
|
|
batchProgress.value = null
|
2025-04-14 10:11:02 +08:00
|
|
|
|
// 获取文档列表
|
|
|
|
|
getDocumentList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化解析状态
|
|
|
|
|
function formatParseStatus(progress: number) {
|
|
|
|
|
if (progress === 0) return "未解析"
|
|
|
|
|
if (progress === 1) return "已完成"
|
|
|
|
|
return `解析中 ${Math.floor(progress * 100)}%`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取解析状态对应的标签类型
|
|
|
|
|
function getParseStatusType(progress: number) {
|
|
|
|
|
if (progress === 0) return "info"
|
|
|
|
|
if (progress === 1) return "success"
|
|
|
|
|
return "warning"
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-19 01:27:37 +08:00
|
|
|
|
// handleParseDocument 方法
|
2025-04-14 10:11:02 +08:00
|
|
|
|
function handleParseDocument(row: any) {
|
|
|
|
|
// 先判断是否已完成解析
|
|
|
|
|
if (row.progress === 1) {
|
|
|
|
|
ElMessage.warning("文档已完成解析,无需再重复解析")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
`确定要解析文档 "${row.name}" 吗?`,
|
|
|
|
|
"解析确认",
|
|
|
|
|
{
|
|
|
|
|
confirmButtonText: "确定",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
type: "info"
|
|
|
|
|
}
|
|
|
|
|
).then(() => {
|
|
|
|
|
runDocumentParseApi(row.id)
|
|
|
|
|
.then(() => {
|
|
|
|
|
ElMessage.success("解析任务已提交")
|
2025-04-14 21:23:11 +08:00
|
|
|
|
// 设置当前文档ID并显示解析进度对话框
|
|
|
|
|
currentDocId.value = row.id
|
|
|
|
|
showParseProgress.value = true
|
2025-04-14 10:11:02 +08:00
|
|
|
|
// 刷新文档列表
|
|
|
|
|
getDocumentList()
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
ElMessage.error(`解析任务提交失败: ${error?.message || "未知错误"}`)
|
|
|
|
|
})
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
// 用户取消操作
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-19 01:27:37 +08:00
|
|
|
|
// 处理批量文档解析 (调用同步 API)
|
|
|
|
|
function handleBatchParse() {
|
|
|
|
|
if (!currentKnowledgeBase.value) return
|
|
|
|
|
|
|
|
|
|
const kbId = currentKnowledgeBase.value.id
|
|
|
|
|
const kbName = currentKnowledgeBase.value.name
|
|
|
|
|
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
`确定要为知识库 "${kbName}" 启动后台批量解析吗?<br><strong style="color: #E6A23C;">该过程将在后台运行,您可以稍后查看结果或关闭此窗口。</strong>`,
|
|
|
|
|
"启动批量解析确认",
|
|
|
|
|
{
|
|
|
|
|
confirmButtonText: "确定启动",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
type: "warning",
|
|
|
|
|
dangerouslyUseHTMLString: true // 允许使用 HTML 标签
|
|
|
|
|
}
|
|
|
|
|
).then(async () => {
|
|
|
|
|
batchParsingLoading.value = true // 标记“正在启动”状态
|
|
|
|
|
batchProgress.value = null
|
|
|
|
|
try {
|
|
|
|
|
const res = await startSequentialBatchParseAsyncApi(kbId)
|
|
|
|
|
|
|
|
|
|
if (res.code === 0 && res.data) {
|
|
|
|
|
// 后端成功接收了启动请求
|
|
|
|
|
ElMessage.success(res.data.message || `已成功启动批量解析任务`)
|
|
|
|
|
// --- 关键:启动轮询来监控进度 ---
|
|
|
|
|
startBatchPolling()
|
|
|
|
|
// 可以在启动后稍微延迟一下再刷新列表,尝试显示“解析中”的状态
|
|
|
|
|
setTimeout(getDocumentList, 1500)
|
|
|
|
|
} else {
|
|
|
|
|
// 启动 API 本身调用失败,或后端返回了错误
|
|
|
|
|
const errorMsg = res.data?.message || res.message || "启动批量解析任务失败"
|
|
|
|
|
ElMessage.error(errorMsg)
|
|
|
|
|
batchParsingLoading.value = false // 启动失败,取消“正在启动”状态
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// 请求启动 API 时发生网络错误或其他异常
|
|
|
|
|
ElMessage.error(`启动批量解析任务时出错: ${error?.message || "网络错误"}`)
|
|
|
|
|
console.error("启动批量解析任务失败:", error)
|
|
|
|
|
batchParsingLoading.value = false // 启动异常,取消“正在启动”状态
|
|
|
|
|
} finally {
|
|
|
|
|
// 只有在 *没有* 成功启动轮询的情况下,才将 batchParsingLoading 设置为 false
|
|
|
|
|
// 如果轮询已开始,则由 isBatchPolling 状态控制按钮和界面的显示
|
|
|
|
|
if (!isBatchPolling.value) {
|
|
|
|
|
batchParsingLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
// 用户点击了“取消”按钮
|
|
|
|
|
ElMessage.info("已取消批量解析操作")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
// 开始轮询批量任务进度
|
|
|
|
|
function startBatchPolling() {
|
|
|
|
|
// 如果已经在轮询或者没有当前知识库,则不执行
|
|
|
|
|
if (isBatchPolling.value || !currentKnowledgeBase.value) return
|
|
|
|
|
|
|
|
|
|
console.log("开始轮询知识库的批量解析进度:", currentKnowledgeBase.value.id)
|
|
|
|
|
isBatchPolling.value = true
|
|
|
|
|
// 设置一个初始状态,给用户即时反馈
|
|
|
|
|
batchProgress.value = { status: "running", message: "正在启动批量解析任务...", total: 0, current: 0 }
|
|
|
|
|
|
|
|
|
|
// 以防万一,先清除可能存在的旧定时器
|
|
|
|
|
if (batchPollingInterval.value) {
|
|
|
|
|
clearInterval(batchPollingInterval.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 立即执行一次获取进度,然后设置定时器
|
|
|
|
|
fetchBatchProgress()
|
|
|
|
|
batchPollingInterval.value = setInterval(fetchBatchProgress, 5000) // 每 5 秒查询一次进度
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 停止轮询批量任务进度
|
|
|
|
|
function stopBatchPolling() {
|
|
|
|
|
if (batchPollingInterval.value) {
|
|
|
|
|
console.log("停止批量解析进度轮询。")
|
|
|
|
|
clearInterval(batchPollingInterval.value)
|
|
|
|
|
batchPollingInterval.value = null
|
|
|
|
|
}
|
|
|
|
|
// 保留最后一次的状态信息用于显示
|
|
|
|
|
isBatchPolling.value = false
|
|
|
|
|
batchParsingLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取并更新批量任务进度
|
|
|
|
|
async function fetchBatchProgress() {
|
|
|
|
|
// 如果详情对话框已关闭或没有当前知识库,则停止轮询
|
|
|
|
|
if (!currentKnowledgeBase.value || !viewDialogVisible.value) {
|
|
|
|
|
stopBatchPolling()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 调用获取进度的 API
|
|
|
|
|
const res = await getSequentialBatchParseProgressApi(currentKnowledgeBase.value.id)
|
|
|
|
|
|
|
|
|
|
if (res.code === 0 && res.data) {
|
|
|
|
|
// 更新进度状态
|
|
|
|
|
batchProgress.value = res.data
|
|
|
|
|
console.log("获取到批量进度:", batchProgress.value)
|
|
|
|
|
|
|
|
|
|
// 检查任务是否已完成或失败
|
|
|
|
|
if (batchProgress.value.status === "completed" || batchProgress.value.status === "failed") {
|
|
|
|
|
stopBatchPolling() // 停止轮询
|
|
|
|
|
// 显示最终结果提示
|
|
|
|
|
ElMessage({
|
|
|
|
|
message: batchProgress.value.message || (batchProgress.value.status === "completed" ? "批量解析已完成" : "批量解析失败"),
|
|
|
|
|
type: batchProgress.value.status === "completed" ? "success" : "error"
|
|
|
|
|
})
|
|
|
|
|
// 刷新文档列表以显示最新状态
|
|
|
|
|
getDocumentList()
|
|
|
|
|
// 刷新知识库列表(文档数、分块数可能变化)
|
|
|
|
|
getTableData()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// API 调用成功但返回了错误码,或者没有数据
|
|
|
|
|
console.error("获取批量进度失败:", res.message || res.data?.message)
|
|
|
|
|
// 可以在界面上提示获取进度时遇到问题
|
|
|
|
|
if (batchProgress.value) { // 确保 batchProgress 不是 null
|
|
|
|
|
batchProgress.value.message = `获取进度时出错: ${res.message || "请稍后..."}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// 网络错误或其他请求异常
|
|
|
|
|
console.error("请求批量进度API失败:", error)
|
|
|
|
|
// 可以在界面上提示网络问题
|
|
|
|
|
if (batchProgress.value) { // 确保 batchProgress 不是 null
|
|
|
|
|
batchProgress.value.message = `获取进度时网络错误: ${error.message || "请检查网络连接..."}`
|
|
|
|
|
}
|
|
|
|
|
// 根据策略决定是否停止轮询,例如连续多次失败后停止
|
|
|
|
|
// stopBatchPolling();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-14 21:23:11 +08:00
|
|
|
|
// 添加解析完成和失败的处理函数
|
|
|
|
|
function handleParseComplete() {
|
|
|
|
|
ElMessage.success("文档解析完成")
|
|
|
|
|
getDocumentList() // 刷新文档列表
|
|
|
|
|
getTableData() // 刷新知识库列表(因为文档数量可能变化)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleParseFailed(error: string) {
|
|
|
|
|
ElMessage.error(`文档解析失败: ${error || "未知错误"}`)
|
|
|
|
|
getDocumentList() // 刷新文档列表以更新状态
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-14 10:11:02 +08:00
|
|
|
|
// 处理移除文档
|
|
|
|
|
function handleRemoveDocument(row: any) {
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
`确定要从知识库中移除文档 "${row.name}" 吗?<br><span style="color: #909399; font-size: 12px;">该操作只是移除知识库文件,不会删除原始文件</span>`,
|
|
|
|
|
"移除确认",
|
|
|
|
|
{
|
|
|
|
|
confirmButtonText: "确定",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
type: "warning",
|
|
|
|
|
dangerouslyUseHTMLString: true
|
|
|
|
|
}
|
|
|
|
|
).then(() => {
|
|
|
|
|
deleteDocumentApi(row.id)
|
|
|
|
|
.then(() => {
|
|
|
|
|
ElMessage.success("文档已从知识库移除")
|
|
|
|
|
// 刷新文档列表
|
|
|
|
|
getDocumentList()
|
|
|
|
|
// 刷新知识库列表(因为文档数量会变化)
|
|
|
|
|
getTableData()
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
ElMessage.error(`移除文档失败: ${error?.message || "未知错误"}`)
|
|
|
|
|
})
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
// 用户取消操作
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加文档对话框
|
|
|
|
|
const addDocumentDialogVisible = ref(false)
|
|
|
|
|
const selectedFiles = ref<string[]>([])
|
|
|
|
|
const fileLoading = ref(false)
|
|
|
|
|
const fileList = ref<any[]>([])
|
|
|
|
|
const filePaginationData = reactive({
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
total: 0,
|
|
|
|
|
pageSizes: [10, 20, 50, 100],
|
|
|
|
|
layout: "total, sizes, prev, pager, next, jumper"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 处理添加文档
|
|
|
|
|
function handleAddDocument() {
|
|
|
|
|
addDocumentDialogVisible.value = true
|
|
|
|
|
// 重置选择
|
|
|
|
|
selectedFiles.value = []
|
|
|
|
|
// 获取文件列表
|
|
|
|
|
getFileList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件列表
|
|
|
|
|
function getFileList() {
|
|
|
|
|
fileLoading.value = true
|
|
|
|
|
// 调用获取文件列表的API
|
|
|
|
|
getFileListApi({
|
|
|
|
|
currentPage: filePaginationData.currentPage,
|
|
|
|
|
size: filePaginationData.pageSize,
|
|
|
|
|
name: ""
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
const typedResponse = response as ApiResponse<FileListResponse>
|
|
|
|
|
fileList.value = typedResponse.data.list
|
|
|
|
|
filePaginationData.total = typedResponse.data.total
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
ElMessage.error(`获取文件列表失败: ${error?.message || "未知错误"}`)
|
|
|
|
|
fileList.value = []
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
fileLoading.value = false
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理文件选择变化
|
|
|
|
|
function handleFileSelectionChange(selection: any[]) {
|
|
|
|
|
// 使用Array.from和JSON方法双重确保转换为普通数组
|
|
|
|
|
selectedFiles.value = JSON.parse(JSON.stringify(Array.from(selection).map(item => item.id)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加一个请求锁变量
|
|
|
|
|
const isAddingDocument = ref(false)
|
|
|
|
|
const messageShown = ref(false) // 添加这一行,将 messageShown 提升为组件级别的变量
|
|
|
|
|
|
|
|
|
|
// 修改 confirmAddDocument 函数
|
|
|
|
|
async function confirmAddDocument() {
|
|
|
|
|
// 检查是否已经在处理请求
|
|
|
|
|
if (isAddingDocument.value) {
|
|
|
|
|
console.log("正在处理添加文档请求,请勿重复点击")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedFiles.value.length === 0) {
|
|
|
|
|
ElMessage.warning("请至少选择一个文件")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentKnowledgeBase.value) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 设置请求锁
|
|
|
|
|
isAddingDocument.value = true
|
|
|
|
|
messageShown.value = false // 重置消息显示标志,使用组件级别的变量
|
|
|
|
|
console.log("开始添加文档请求...", selectedFiles.value)
|
|
|
|
|
|
|
|
|
|
// 直接处理文件ID,不再弹出确认对话框
|
|
|
|
|
const fileIds = JSON.parse(JSON.stringify([...selectedFiles.value]))
|
|
|
|
|
|
|
|
|
|
// 发送API请求 - 移除不必要的内层 try/catch
|
|
|
|
|
const response = await axios.post(
|
|
|
|
|
`/api/v1/knowledgebases/${currentKnowledgeBase.value.id}/documents`,
|
|
|
|
|
{ file_ids: fileIds }
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.log("API原始响应:", response)
|
|
|
|
|
|
|
|
|
|
// 检查响应状态
|
|
|
|
|
if (response.data && (response.data.code === 0 || response.data.code === 201)) {
|
|
|
|
|
// 成功处理
|
|
|
|
|
if (!messageShown.value) {
|
|
|
|
|
messageShown.value = true
|
|
|
|
|
console.log("显示成功消息")
|
|
|
|
|
ElMessage.success("文档添加成功")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addDocumentDialogVisible.value = false
|
|
|
|
|
getDocumentList()
|
|
|
|
|
getTableData()
|
|
|
|
|
} else {
|
|
|
|
|
// 处理错误响应
|
|
|
|
|
throw new Error(response.data?.message || "添加文档失败")
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// API调用失败
|
|
|
|
|
console.error("API请求失败详情:", {
|
|
|
|
|
error: error?.toString(),
|
|
|
|
|
stack: error?.stack,
|
|
|
|
|
response: error?.response?.data,
|
|
|
|
|
request: error?.request,
|
|
|
|
|
config: error?.config
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 添加更详细的错误日志
|
|
|
|
|
console.log("错误详情:", error)
|
|
|
|
|
if (error.response) {
|
|
|
|
|
console.log("响应数据:", error.response.data)
|
|
|
|
|
console.log("响应状态:", error.response.status)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ElMessage.error(`添加文档失败: ${error?.message || "未知错误"}`)
|
|
|
|
|
} finally {
|
|
|
|
|
// 无论成功失败,都解除请求锁
|
|
|
|
|
console.log("添加文档请求完成,解除锁定", new Date().toISOString())
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
isAddingDocument.value = false
|
|
|
|
|
}, 500) // 增加延迟,防止快速点击
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化文件大小
|
|
|
|
|
function formatFileSize(size: number) {
|
|
|
|
|
if (!size) return "0 B"
|
|
|
|
|
|
|
|
|
|
const units = ["B", "KB", "MB", "GB", "TB"]
|
|
|
|
|
let index = 0
|
|
|
|
|
while (size >= 1024 && index < units.length - 1) {
|
|
|
|
|
size /= 1024
|
|
|
|
|
index++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${size.toFixed(2)} ${units[index]}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化文件类型
|
|
|
|
|
function formatFileType(type: string) {
|
|
|
|
|
const typeMap: Record<string, string> = {
|
|
|
|
|
pdf: "PDF",
|
|
|
|
|
doc: "Word",
|
|
|
|
|
docx: "Word",
|
|
|
|
|
xls: "Excel",
|
|
|
|
|
xlsx: "Excel",
|
|
|
|
|
ppt: "PPT",
|
|
|
|
|
pptx: "PPT",
|
|
|
|
|
txt: "文本",
|
|
|
|
|
md: "Markdown",
|
|
|
|
|
jpg: "图片",
|
|
|
|
|
jpeg: "图片",
|
|
|
|
|
png: "图片"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return typeMap[type.toLowerCase()] || type
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除知识库
|
|
|
|
|
function handleDelete(row: KnowledgeBaseData) {
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
`确定要删除知识库 "${row.name}" 吗?删除后将无法恢复,且其中的所有文档也将被删除。`,
|
|
|
|
|
"删除确认",
|
|
|
|
|
{
|
|
|
|
|
confirmButtonText: "确定",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
type: "warning",
|
|
|
|
|
dangerouslyUseHTMLString: true,
|
|
|
|
|
center: true,
|
|
|
|
|
customClass: "delete-confirm-dialog",
|
|
|
|
|
distinguishCancelAndClose: true,
|
|
|
|
|
showClose: false,
|
|
|
|
|
closeOnClickModal: false,
|
|
|
|
|
closeOnPressEscape: true,
|
|
|
|
|
roundButton: true,
|
|
|
|
|
beforeClose: (action, instance, done) => {
|
|
|
|
|
if (action === "confirm") {
|
|
|
|
|
instance.confirmButtonLoading = true
|
|
|
|
|
instance.confirmButtonText = "删除中..."
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
deleteKnowledgeBaseApi(row.id)
|
|
|
|
|
.then(() => {
|
|
|
|
|
ElMessage.success("删除成功")
|
|
|
|
|
getTableData() // 刷新表格数据
|
|
|
|
|
done()
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
ElMessage.error(`删除失败: ${error?.message || "未知错误"}`)
|
|
|
|
|
done()
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
instance.confirmButtonLoading = false
|
|
|
|
|
loading.value = false
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
done()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
).catch(() => {
|
|
|
|
|
// 用户取消删除操作
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量删除知识库
|
|
|
|
|
function handleBatchDelete() {
|
|
|
|
|
if (multipleSelection.value.length === 0) {
|
|
|
|
|
ElMessage.warning("请至少选择一个知识库")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
`确定要删除选中的 <strong>${multipleSelection.value.length}</strong> 个知识库吗?<br><span style="color: #F56C6C; font-size: 12px;">此操作不可恢复,且其中的所有文档也将被删除</span>`,
|
|
|
|
|
"批量删除确认",
|
|
|
|
|
{
|
|
|
|
|
confirmButtonText: "确定",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
type: "warning",
|
|
|
|
|
dangerouslyUseHTMLString: true,
|
|
|
|
|
center: true,
|
|
|
|
|
customClass: "delete-confirm-dialog",
|
|
|
|
|
distinguishCancelAndClose: true,
|
|
|
|
|
showClose: false,
|
|
|
|
|
closeOnClickModal: false,
|
|
|
|
|
closeOnPressEscape: true,
|
|
|
|
|
roundButton: true,
|
|
|
|
|
beforeClose: (action, instance, done) => {
|
|
|
|
|
if (action === "confirm") {
|
|
|
|
|
instance.confirmButtonLoading = true
|
|
|
|
|
instance.confirmButtonText = "删除中..."
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
const ids = multipleSelection.value.map(item => item.id)
|
|
|
|
|
batchDeleteKnowledgeBaseApi(ids)
|
|
|
|
|
.then(() => {
|
|
|
|
|
ElMessage.success(`成功删除 ${multipleSelection.value.length} 个知识库`)
|
|
|
|
|
getTableData() // 刷新表格数据
|
|
|
|
|
done()
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
ElMessage.error(`批量删除失败: ${error?.message || "未知错误"}`)
|
|
|
|
|
done()
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
instance.confirmButtonLoading = false
|
|
|
|
|
loading.value = false
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
done()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
).catch(() => {
|
|
|
|
|
// 用户取消删除操作
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表格多选事件处理
|
|
|
|
|
function handleSelectionChange(selection: KnowledgeBaseData[]) {
|
|
|
|
|
multipleSelection.value = selection
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 监听分页参数的变化
|
|
|
|
|
watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
|
|
|
|
|
|
|
|
|
|
// 确保页面挂载和激活时获取数据
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
getTableData()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 当从其他页面切换回来时刷新数据
|
|
|
|
|
onActivated(() => {
|
|
|
|
|
getTableData()
|
|
|
|
|
})
|
2025-04-18 22:34:25 +08:00
|
|
|
|
|
|
|
|
|
// 系统 Embedding 配置逻辑
|
|
|
|
|
const configModalVisible = ref(false)
|
|
|
|
|
const configFormRef = ref<FormInstance>() // 表单引用
|
|
|
|
|
const configFormLoading = ref(false) // 表单加载状态
|
|
|
|
|
const configSubmitLoading = ref(false) // 提交按钮加载状态
|
|
|
|
|
|
|
|
|
|
const configForm = reactive({
|
|
|
|
|
llm_name: "",
|
|
|
|
|
api_base: "",
|
|
|
|
|
api_key: ""
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 简单的 URL 校验规则
|
|
|
|
|
function validateUrl(rule: any, value: any, callback: any) {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return callback(new Error("请输入模型 API 地址"))
|
|
|
|
|
}
|
|
|
|
|
// 允许 http, https 开头,允许 IP 地址和域名,允许端口
|
|
|
|
|
// 修正:允许路径,但不允许查询参数或片段
|
|
|
|
|
const urlPattern = /^(https?:\/\/)?([a-zA-Z0-9.-]+|\[[a-fA-F0-9:]+\])(:\d+)?(\/[^?#]*)?$/
|
|
|
|
|
if (!urlPattern.test(value)) {
|
|
|
|
|
callback(new Error("请输入有效的 Base URL (例如 http://host:port 或 https://domain/path)"))
|
|
|
|
|
} else {
|
|
|
|
|
callback()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const configFormRules = reactive({
|
|
|
|
|
llm_name: [{ required: true, message: "请输入模型名称", trigger: "blur" }],
|
|
|
|
|
api_base: [{ required: true, validator: validateUrl, trigger: "blur" }]
|
|
|
|
|
// api_key 不是必填项
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 显示配置模态框
|
|
|
|
|
async function showConfigModal() {
|
|
|
|
|
configModalVisible.value = true
|
|
|
|
|
configFormLoading.value = true
|
|
|
|
|
// 重置表单可能需要在 nextTick 中执行,确保 DOM 更新完毕
|
|
|
|
|
await nextTick()
|
|
|
|
|
configFormRef.value?.resetFields() // 清空上次的输入和校验状态
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 确认 API 函数名称是否正确,并添加类型断言
|
|
|
|
|
const res = await getSystemEmbeddingConfigApi() as ApiResponse<{ llm_name?: string, api_base?: string, api_key?: string }>
|
|
|
|
|
if (res.code === 0 && res.data) {
|
|
|
|
|
configForm.llm_name = res.data.llm_name || ""
|
|
|
|
|
configForm.api_base = res.data.api_base || ""
|
|
|
|
|
// 注意:API Key 通常不应在 GET 请求中返回,如果后端不返回,这里会是空字符串
|
|
|
|
|
configForm.api_key = res.data.api_key || ""
|
|
|
|
|
} else if (res.code !== 0) {
|
|
|
|
|
ElMessage.error(res.message || "获取配置失败")
|
|
|
|
|
} else {
|
|
|
|
|
// code === 0 但 data 为空,说明没有配置
|
2025-04-19 01:27:37 +08:00
|
|
|
|
console.log("当前未配置嵌入模型。")
|
2025-04-18 22:34:25 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
ElMessage.error(error.message || "获取配置请求失败")
|
|
|
|
|
console.error("获取配置失败:", error)
|
|
|
|
|
} finally {
|
|
|
|
|
configFormLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理模态框关闭
|
|
|
|
|
function handleModalClose() {
|
|
|
|
|
// 可以在这里再次重置表单,以防用户未保存直接关闭
|
|
|
|
|
configFormRef.value?.resetFields()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理配置提交
|
|
|
|
|
async function handleConfigSubmit() {
|
|
|
|
|
if (!configFormRef.value) return
|
|
|
|
|
// 使用 .then() .catch() 处理 validate 的 Promise
|
|
|
|
|
configFormRef.value.validate().then(async () => {
|
|
|
|
|
// 验证通过
|
|
|
|
|
configSubmitLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const payload = {
|
|
|
|
|
llm_name: configForm.llm_name.trim(),
|
|
|
|
|
api_base: configForm.api_base.trim(),
|
|
|
|
|
api_key: configForm.api_key
|
|
|
|
|
}
|
|
|
|
|
// 确认 API 函数名称是否正确,并添加类型断言
|
|
|
|
|
const res = await setSystemEmbeddingConfigApi(payload) as ApiResponse<any> // 使用类型断言并指定泛型参数为any
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
ElMessage.success("连接验证成功!")
|
|
|
|
|
configModalVisible.value = false
|
|
|
|
|
} else {
|
|
|
|
|
// 后端应在 res.message 中返回错误信息,包括连接测试失败的原因
|
|
|
|
|
ElMessage.error(res.message || "连接验证失败")
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
ElMessage.error(error.message || "连接验证请求失败")
|
|
|
|
|
console.error("连接验证失败:", error)
|
|
|
|
|
} finally {
|
|
|
|
|
configSubmitLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}).catch((errorFields) => {
|
|
|
|
|
// 验证失败
|
|
|
|
|
console.log("表单验证失败!", errorFields)
|
|
|
|
|
// 这里不需要返回 false,validate 的 Promise reject 就表示失败了
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-04-19 01:27:37 +08:00
|
|
|
|
|
|
|
|
|
// 根据状态决定 Alert 类型
|
|
|
|
|
function getAlertType(status: any) {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case "failed":
|
|
|
|
|
case "not_found": // 'not_found' 可能也视为一种错误
|
|
|
|
|
return "error"
|
|
|
|
|
case "completed":
|
|
|
|
|
return "success"
|
|
|
|
|
case "cancelled":
|
|
|
|
|
return "warning" // 取消可以视为警告或信息,看需求
|
|
|
|
|
case "running":
|
|
|
|
|
case "starting":
|
|
|
|
|
case "cancelling":
|
|
|
|
|
default:
|
|
|
|
|
return "info"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 判断是否是加载中状态
|
|
|
|
|
function isLoadingStatus(status: string) {
|
|
|
|
|
return ["running", "starting", "cancelling"].includes(status)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 判断是否应该显示进度计数 (例如,启动中或任务未找到时可能不适合显示 0/0)
|
|
|
|
|
function shouldShowProgressCount(status: string) {
|
|
|
|
|
return !["starting", "not_found"].includes(status)
|
|
|
|
|
}
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<div>
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
<el-card v-loading="loading" shadow="never" class="search-wrapper">
|
|
|
|
|
<el-form ref="searchFormRef" :inline="true" :model="searchData">
|
|
|
|
|
<el-form-item prop="name" label="知识库名称">
|
|
|
|
|
<el-input v-model="searchData.name" placeholder="请输入" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button type="primary" :icon="Search" @click="handleSearch">
|
|
|
|
|
搜索
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button :icon="Refresh" @click="resetSearch">
|
|
|
|
|
重置
|
|
|
|
|
</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
<el-card v-loading="loading" shadow="never">
|
|
|
|
|
<div class="toolbar-wrapper">
|
2025-04-14 10:11:02 +08:00
|
|
|
|
<div>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
|
|
|
|
:icon="Plus"
|
|
|
|
|
@click="handleCreate"
|
|
|
|
|
>
|
|
|
|
|
新建知识库
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button
|
|
|
|
|
type="danger"
|
|
|
|
|
:icon="Delete"
|
|
|
|
|
:disabled="multipleSelection.length === 0"
|
|
|
|
|
@click="handleBatchDelete"
|
|
|
|
|
>
|
|
|
|
|
批量删除
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
2025-04-18 22:34:25 +08:00
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<el-button type="primary" :icon="Setting" @click="showConfigModal">
|
2025-04-19 01:27:37 +08:00
|
|
|
|
嵌入模型配置
|
2025-04-18 22:34:25 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<div class="table-wrapper">
|
|
|
|
|
<el-table :data="tableData" @selection-change="handleSelectionChange">
|
|
|
|
|
<el-table-column type="selection" width="50" align="center" />
|
|
|
|
|
<el-table-column label="序号" align="center" width="80">
|
2025-04-14 10:11:02 +08:00
|
|
|
|
<template #default="scope">
|
2025-04-16 22:22:28 +08:00
|
|
|
|
{{ (paginationData.currentPage - 1) * paginationData.pageSize + scope.$index + 1 }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="name" label="知识库名称" align="center" min-width="120" />
|
|
|
|
|
<el-table-column prop="description" label="描述" align="center" min-width="180" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="doc_num" label="文档数量" align="center" width="100" />
|
|
|
|
|
<!-- 添加语言列 -->
|
|
|
|
|
<el-table-column label="语言" align="center" width="100">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-tag type="info" size="small">
|
|
|
|
|
{{ scope.row.language === 'Chinese' ? '中文' : '英文' }}
|
|
|
|
|
</el-tag>
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<!-- 添加权限列 -->
|
|
|
|
|
<el-table-column label="权限" align="center" width="100">
|
2025-04-14 10:11:02 +08:00
|
|
|
|
<template #default="scope">
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<el-tag :type="scope.row.permission === 'me' ? 'success' : 'warning'" size="small">
|
|
|
|
|
{{ scope.row.permission === 'me' ? '个人' : '团队' }}
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<el-table-column label="创建时间" align="center" width="180">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
{{ scope.row.create_date }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column fixed="right" label="操作" width="180" align="center">
|
2025-04-14 10:11:02 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-button
|
2025-04-16 22:22:28 +08:00
|
|
|
|
type="primary"
|
|
|
|
|
text
|
|
|
|
|
bg
|
2025-04-14 10:11:02 +08:00
|
|
|
|
size="small"
|
2025-04-16 22:22:28 +08:00
|
|
|
|
:icon="View"
|
|
|
|
|
@click="handleView(scope.row)"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
查看
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
<el-button
|
|
|
|
|
type="danger"
|
2025-04-16 22:22:28 +08:00
|
|
|
|
text
|
|
|
|
|
bg
|
2025-04-14 10:11:02 +08:00
|
|
|
|
size="small"
|
|
|
|
|
:icon="Delete"
|
2025-04-16 22:22:28 +08:00
|
|
|
|
@click="handleDelete(scope.row)"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
删除
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<div class="pager-wrapper">
|
2025-04-14 10:11:02 +08:00
|
|
|
|
<el-pagination
|
2025-04-16 22:22:28 +08:00
|
|
|
|
background
|
|
|
|
|
:layout="paginationData.layout"
|
|
|
|
|
:page-sizes="paginationData.pageSizes"
|
|
|
|
|
:total="paginationData.total"
|
|
|
|
|
:page-size="paginationData.pageSize"
|
|
|
|
|
:current-page="paginationData.currentPage"
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
@current-change="handleCurrentChange"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
</el-card>
|
2025-04-14 10:11:02 +08:00
|
|
|
|
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<!-- 知识库详情对话框 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="viewDialogVisible"
|
|
|
|
|
:title="`知识库详情 - ${currentKnowledgeBase?.name || ''}`"
|
|
|
|
|
width="80%"
|
2025-04-19 01:27:37 +08:00
|
|
|
|
append-to-body
|
|
|
|
|
:close-on-click-modal="!batchParsingLoading"
|
|
|
|
|
:close-on-press-escape="!batchParsingLoading"
|
|
|
|
|
:show-close="!batchParsingLoading"
|
2025-04-16 22:22:28 +08:00
|
|
|
|
>
|
|
|
|
|
<div v-if="currentKnowledgeBase">
|
|
|
|
|
<div class="kb-info-header">
|
|
|
|
|
<div>
|
|
|
|
|
<span class="kb-info-label">知识库ID:</span> {{ currentKnowledgeBase.id }}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span class="kb-info-label">文档总数:</span> {{ currentKnowledgeBase.doc_num }}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span class="kb-info-label">语言:</span>
|
|
|
|
|
<el-tag type="info" size="small">
|
|
|
|
|
{{ currentKnowledgeBase.language === 'Chinese' ? '中文' : '英文' }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span class="kb-info-label">权限:</span>
|
|
|
|
|
<el-tag :type="currentKnowledgeBase.permission === 'me' ? 'success' : 'warning'" size="small">
|
|
|
|
|
{{ currentKnowledgeBase.permission === 'me' ? '个人' : '团队' }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="document-table-header">
|
|
|
|
|
<div class="left-buttons">
|
|
|
|
|
<el-button type="primary" @click="handleAddDocument">
|
|
|
|
|
添加文档
|
|
|
|
|
</el-button>
|
2025-04-19 01:27:37 +08:00
|
|
|
|
<!-- 批量解析按钮 -->
|
|
|
|
|
<el-button
|
|
|
|
|
type="warning"
|
|
|
|
|
:icon="CaretRight"
|
|
|
|
|
:loading="batchParsingLoading && !isBatchPolling"
|
|
|
|
|
@click="handleBatchParse"
|
|
|
|
|
:disabled="isBatchParseDisabled || batchParsingLoading"
|
|
|
|
|
>
|
|
|
|
|
<!-- 根据是否在轮询显示不同文本 -->
|
|
|
|
|
{{ isBatchPolling ? '正在批量解析...' : (batchParsingLoading ? '正在启动...' : '批量解析') }}
|
|
|
|
|
</el-button>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-04-19 01:27:37 +08:00
|
|
|
|
<!-- 进度显示 -->
|
|
|
|
|
<div v-if="batchProgress" class="batch-progress">
|
|
|
|
|
<el-alert
|
|
|
|
|
:title="batchProgress.message || '正在处理...'"
|
|
|
|
|
:type="getAlertType(batchProgress.status)"
|
|
|
|
|
:closable="false"
|
|
|
|
|
show-icon
|
|
|
|
|
class="batch-progress-alert"
|
|
|
|
|
>
|
|
|
|
|
<!-- 加载图标:仅在进行中相关状态 ('running', 'starting', 'cancelling') 显示 -->
|
|
|
|
|
<template #icon v-if="isLoadingStatus(batchProgress.status)">
|
|
|
|
|
<el-icon class="is-loading">
|
|
|
|
|
<Loading />
|
|
|
|
|
</el-icon>
|
|
|
|
|
</template>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
|
2025-04-19 01:27:37 +08:00
|
|
|
|
<!-- 默认插槽:用于显示额外的进度详情 -->
|
|
|
|
|
<div class="batch-progress-details">
|
|
|
|
|
<!-- 显示处理进度 (当前项 / 总项数) -->
|
|
|
|
|
<!-- 仅当总数大于0且状态不是 'starting' 或 'not_found' 时显示比较有意义 -->
|
|
|
|
|
<template v-if="batchProgress.total > 0 && shouldShowProgressCount(batchProgress.status)">
|
|
|
|
|
<p>
|
|
|
|
|
处理进度: {{ batchProgress.current ?? 0 }} / {{ batchProgress.total }}
|
|
|
|
|
</p>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 占位符:如果没有显示进度计数,则显示一个占位符防止高度塌陷 -->
|
|
|
|
|
<template v-else>
|
|
|
|
|
<p /> <!-- 使用不换行空格确保最小高度 -->
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</el-alert>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- === 结束进度显示 === -->
|
|
|
|
|
|
|
|
|
|
<div class="document-table-wrapper" v-loading="documentLoading || (isBatchPolling && !batchProgress)">
|
2025-04-16 22:22:28 +08:00
|
|
|
|
<el-table :data="documentList" style="width: 100%">
|
|
|
|
|
<el-table-column prop="name" label="名称" min-width="180" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="chunk_num" label="分块数" width="100" align="center" />
|
|
|
|
|
<el-table-column label="上传日期" width="180" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
{{ scope.row.create_date }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="解析状态" width="120" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-tag :type="getParseStatusType(scope.row.progress)">
|
|
|
|
|
{{ formatParseStatus(scope.row.progress) }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="200" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-button
|
|
|
|
|
type="success"
|
|
|
|
|
size="small"
|
|
|
|
|
:icon="CaretRight"
|
|
|
|
|
@click="handleParseDocument(scope.row)"
|
|
|
|
|
>
|
|
|
|
|
解析
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button
|
|
|
|
|
type="danger"
|
|
|
|
|
size="small"
|
|
|
|
|
:icon="Delete"
|
|
|
|
|
@click="handleRemoveDocument(scope.row)"
|
|
|
|
|
>
|
|
|
|
|
移除
|
|
|
|
|
</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<!-- 分页控件 -->
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="docPaginationData.currentPage"
|
|
|
|
|
v-model:page-size="docPaginationData.pageSize"
|
|
|
|
|
:page-sizes="docPaginationData.pageSizes"
|
|
|
|
|
:layout="docPaginationData.layout"
|
|
|
|
|
:total="docPaginationData.total"
|
|
|
|
|
@size-change="handleDocSizeChange"
|
|
|
|
|
@current-change="handleDocCurrentChange"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- 新建知识库对话框 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="createDialogVisible"
|
|
|
|
|
title="新建知识库"
|
|
|
|
|
width="40%"
|
|
|
|
|
>
|
|
|
|
|
<el-form
|
|
|
|
|
ref="knowledgeBaseFormRef"
|
|
|
|
|
:model="knowledgeBaseForm"
|
|
|
|
|
:rules="knowledgeBaseFormRules"
|
|
|
|
|
label-width="100px"
|
|
|
|
|
>
|
|
|
|
|
<el-form-item label="知识库名称" prop="name">
|
|
|
|
|
<el-input v-model="knowledgeBaseForm.name" placeholder="请输入知识库名称" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="描述" prop="description">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="knowledgeBaseForm.description"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="3"
|
|
|
|
|
placeholder="请输入知识库描述"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="语言" prop="language">
|
|
|
|
|
<el-select v-model="knowledgeBaseForm.language" placeholder="请选择语言">
|
|
|
|
|
<el-option label="中文" value="Chinese" />
|
|
|
|
|
<el-option label="英文" value="English" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="权限" prop="permission">
|
|
|
|
|
<el-select v-model="knowledgeBaseForm.permission" placeholder="请选择权限">
|
|
|
|
|
<el-option label="个人" value="me" />
|
|
|
|
|
<el-option label="团队" value="team" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="createDialogVisible = false">
|
|
|
|
|
取消
|
|
|
|
|
</el-button>
|
2025-04-14 10:11:02 +08:00
|
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
2025-04-16 22:22:28 +08:00
|
|
|
|
:loading="uploadLoading"
|
|
|
|
|
@click="submitCreate"
|
2025-04-14 10:11:02 +08:00
|
|
|
|
>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
确认创建
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</el-button>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- 添加文档对话框 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="addDocumentDialogVisible"
|
|
|
|
|
title="添加文档到知识库"
|
|
|
|
|
width="70%"
|
|
|
|
|
>
|
|
|
|
|
<div v-loading="fileLoading">
|
|
|
|
|
<el-table
|
|
|
|
|
:data="fileList"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
@selection-change="handleFileSelectionChange"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column type="selection" width="55" />
|
|
|
|
|
<el-table-column prop="name" label="文件名" min-width="180" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="size" label="大小" width="100" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
{{ formatFileSize(scope.row.size) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="type" label="类型" width="100" align="center">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
{{ formatFileType(scope.row.type) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<!-- 移除上传日期列 -->
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<!-- 分页控件 -->
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="filePaginationData.currentPage"
|
|
|
|
|
v-model:page-size="filePaginationData.pageSize"
|
|
|
|
|
:page-sizes="filePaginationData.pageSizes"
|
|
|
|
|
:layout="filePaginationData.layout"
|
|
|
|
|
:total="filePaginationData.total"
|
|
|
|
|
@size-change="(size) => { filePaginationData.pageSize = size; filePaginationData.currentPage = 1; getFileList(); }"
|
|
|
|
|
@current-change="(page) => { filePaginationData.currentPage = page; getFileList(); }"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
<el-button @click="addDocumentDialogVisible = false">取消</el-button>
|
|
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
|
|
|
|
:disabled="isAddingDocument"
|
|
|
|
|
@click.stop.prevent="confirmAddDocument"
|
|
|
|
|
>
|
|
|
|
|
{{ isAddingDocument ? '处理中...' : '确定' }}
|
|
|
|
|
</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
2025-04-18 22:34:25 +08:00
|
|
|
|
|
|
|
|
|
<!-- 系统 Embedding 配置模态框 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
v-model="configModalVisible"
|
2025-04-19 01:27:37 +08:00
|
|
|
|
title="嵌入模型配置"
|
2025-04-18 22:34:25 +08:00
|
|
|
|
width="500px"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
@close="handleModalClose"
|
|
|
|
|
append-to-body
|
|
|
|
|
>
|
|
|
|
|
<el-form
|
|
|
|
|
ref="configFormRef"
|
|
|
|
|
:model="configForm"
|
|
|
|
|
:rules="configFormRules"
|
|
|
|
|
label-width="120px"
|
|
|
|
|
v-loading="configFormLoading"
|
|
|
|
|
>
|
|
|
|
|
<el-form-item label="模型名称" prop="llm_name">
|
|
|
|
|
<el-input v-model="configForm.llm_name" placeholder="请先在前台进行配置" disabled />
|
|
|
|
|
<div class="form-tip">
|
|
|
|
|
与模型服务中部署的名称一致
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="模型 API 地址" prop="api_base">
|
|
|
|
|
<el-input v-model="configForm.api_base" placeholder="请先在前台进行配置" disabled />
|
|
|
|
|
<div class="form-tip">
|
|
|
|
|
模型的 Base URL
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="API Key (可选)" prop="api_key">
|
|
|
|
|
<el-input v-model="configForm.api_key" type="password" show-password placeholder="请先在前台进行配置" disabled />
|
|
|
|
|
<div class="form-tip">
|
|
|
|
|
如果模型服务需要认证,请提供
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<div style="color: #909399; font-size: 12px; line-height: 1.5;">
|
|
|
|
|
此配置将作为知识库解析时默认的 Embedding 模型。
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<span class="dialog-footer">
|
|
|
|
|
<el-button @click="configModalVisible = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="handleConfigSubmit" :loading="configSubmitLoading">
|
|
|
|
|
测试连接
|
|
|
|
|
</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
2025-04-16 22:22:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
<DocumentParseProgress
|
|
|
|
|
:document-id="currentDocId"
|
|
|
|
|
:visible="showParseProgress"
|
|
|
|
|
@close="showParseProgress = false"
|
|
|
|
|
@parse-complete="handleParseComplete"
|
|
|
|
|
@parse-failed="handleParseFailed"
|
|
|
|
|
/>
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2025-04-15 00:35:33 +08:00
|
|
|
|
.app-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
min-height: 94%;
|
|
|
|
|
}
|
2025-04-14 10:11:02 +08:00
|
|
|
|
.el-alert {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-wrapper {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
padding-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar-wrapper {
|
|
|
|
|
display: flex;
|
2025-04-18 22:34:25 +08:00
|
|
|
|
justify-content: space-between; // 确保左右两边对齐
|
|
|
|
|
align-items: center; // 垂直居中对齐
|
2025-04-14 10:11:02 +08:00
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-wrapper {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pager-wrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kb-info-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
.kb-info-label {
|
|
|
|
|
color: #606266;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.document-table-wrapper {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.document-table-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
margin-top: 16px;
|
2025-04-19 01:27:37 +08:00
|
|
|
|
.left-buttons {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
2025-04-14 10:11:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination-container {
|
|
|
|
|
margin-top: 20px;
|
2025-04-15 00:35:33 +08:00
|
|
|
|
margin-bottom: 20px;
|
2025-04-14 10:11:02 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.delete-confirm-dialog {
|
|
|
|
|
:deep(.el-message-box__message) {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-18 22:34:25 +08:00
|
|
|
|
|
|
|
|
|
.form-tip {
|
|
|
|
|
color: #909399;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
2025-04-19 01:27:37 +08:00
|
|
|
|
|
|
|
|
|
.batch-progress {
|
|
|
|
|
margin-bottom: 20px; // 与下方表格的间距
|
|
|
|
|
margin-top: 5px; // 与上方按钮行的间距
|
|
|
|
|
padding: 0 10px; // 左右留一些边距
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.batch-progress-alert {
|
|
|
|
|
// 可以给 Alert 添加一些效果,例如轻微的边框或背景
|
|
|
|
|
// background-color: #f8f8f9; // 非常浅的背景色
|
|
|
|
|
border: 1px solid #e9e9eb;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
// 调整内部 icon 和 title/description 的对齐方式
|
|
|
|
|
:deep(.el-alert__content) {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center; // 垂直居中对齐 Title 和 Icon (如果默认图标显示)
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-alert__title) {
|
|
|
|
|
margin-right: 15px; // 标题和右侧详情之间加点距离
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 自定义加载图标样式
|
|
|
|
|
.el-icon.is-loading {
|
|
|
|
|
margin-right: 8px; // 图标和标题之间的距离
|
|
|
|
|
font-size: 16px; // 图标大小
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.batch-progress-details {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
color: #606266; // 常规细节文字颜色
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
margin: 0; // 移除段落默认边距
|
|
|
|
|
min-height: 18px; // 保证有内容或占位符时有最小高度
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-detail {
|
|
|
|
|
color: #f56c6c; // 错误详情用醒目的红色
|
|
|
|
|
font-weight: 500; // 可以稍微加粗
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保旋转动画
|
|
|
|
|
@keyframes rotating {
|
|
|
|
|
from {
|
|
|
|
|
transform: rotate(0deg);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载图标继承动画
|
|
|
|
|
.el-tag .el-icon.is-loading,
|
|
|
|
|
.batch-progress-alert .el-icon.is-loading {
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
animation: rotating 2s linear infinite;
|
|
|
|
|
}
|
2025-04-14 10:11:02 +08:00
|
|
|
|
</style>
|