【新增】前端代码第一次提交

This commit is contained in:
YOHO\20373
2025-04-17 16:42:02 +08:00
committed by 陆光LG
parent 56df17f7ad
commit 3c1e09aad7
1634 changed files with 237344 additions and 23 deletions

View File

@@ -0,0 +1,146 @@
<template>
<div>
<!-- 文件处理列表 -->
<div class="mt-15px grid grid-cols-1 gap-2">
<div
v-for="(file, index) in modelValue.list"
:key="index"
class="flex items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
>
<!-- 文件图标和名称 -->
<div class="flex items-center min-w-[200px] mr-10px">
<Icon icon="ep:document" class="mr-8px text-[#409eff]" />
<span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
</div>
<!-- 处理进度 -->
<div class="flex-1">
<el-progress
:percentage="file.progress || 0"
:stroke-width="10"
:status="isProcessComplete(file) ? 'success' : ''"
/>
</div>
<!-- 分段数量 -->
<div class="ml-10px text-[13px] text-[#606266]">
分段数量{{ file.count ? file.count : '-' }}
</div>
</div>
</div>
<!-- 底部完成按钮 -->
<div class="flex justify-end mt-20px">
<el-button
:type="allProcessComplete ? 'success' : 'primary'"
:disabled="!allProcessComplete"
@click="handleComplete"
>
完成
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const parent = inject('parent') as any
const pollingTimer = ref<number | null>(null) // 轮询定时器 ID用于跟踪和清除轮询进程
/** 判断文件处理是否完成 */
const isProcessComplete = (file) => {
return file.progress === 100
}
/** 判断所有文件是否都处理完成 */
const allProcessComplete = computed(() => {
return props.modelValue.list.every((file) => isProcessComplete(file))
})
/** 完成按钮点击事件处理 */
const handleComplete = () => {
if (parent?.exposed?.handleBack) {
parent.exposed.handleBack()
}
}
/** 获取文件处理进度 */
const getProcessList = async () => {
try {
// 1. 调用 API 获取处理进度
const documentIds = props.modelValue.list.filter((item) => item.id).map((item) => item.id)
if (documentIds.length === 0) {
return
}
const result = await KnowledgeSegmentApi.getKnowledgeSegmentProcessList(documentIds)
// 2.1更新进度
const updatedList = props.modelValue.list.map((file) => {
const processInfo = result.find((item) => item.documentId === file.id)
if (processInfo) {
// 计算进度百分比:已嵌入数量 / 总数量 * 100
const progress =
processInfo.embeddingCount && processInfo.count
? Math.floor((processInfo.embeddingCount / processInfo.count) * 100)
: 0
return {
...file,
progress: progress,
count: processInfo.count || 0
}
}
return file
})
// 2.2 更新数据
emit('update:modelValue', {
...props.modelValue,
list: updatedList
})
// 3. 如果未完成,继续轮询
if (!updatedList.every((file) => isProcessComplete(file))) {
pollingTimer.value = window.setTimeout(getProcessList, 3000)
}
} catch (error) {
// 出错后也继续轮询
console.error('获取处理进度失败:', error)
pollingTimer.value = window.setTimeout(getProcessList, 5000)
}
}
/** 组件挂载时开始轮询 */
onMounted(() => {
// 1. 初始化进度为 0
const initialList = props.modelValue.list.map((file) => ({
...file,
progress: 0
}))
emit('update:modelValue', {
...props.modelValue,
list: initialList
})
// 2. 开始轮询获取进度
getProcessList()
})
/** 组件卸载前清除轮询 */
onBeforeUnmount(() => {
// 1. 清除定时器
if (pollingTimer.value) {
clearTimeout(pollingTimer.value)
pollingTimer.value = null
}
})
</script>

View File

@@ -0,0 +1,238 @@
<template>
<div>
<!-- 上部分段设置部分 -->
<div class="mb-20px">
<div class="mb-20px flex justify-between items-center">
<div class="text-16px font-bold flex items-center">
分段设置
<el-tooltip
content="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。"
placement="top"
>
<Icon icon="ep:warning" class="ml-5px text-gray-400" />
</el-tooltip>
</div>
<div>
<el-button type="primary" plain size="small" @click="handleAutoSegment">
预览分段
</el-button>
</div>
</div>
<div class="segment-settings mb-20px">
<el-form label-width="120px">
<el-form-item label="最大 Token 数">
<el-input-number v-model="modelData.segmentMaxTokens" :min="1" :max="2048" />
</el-form-item>
</el-form>
</div>
</div>
<!-- 下部文件预览部分 -->
<div class="mb-10px">
<div class="text-16px font-bold mb-10px">分段预览</div>
<!-- 文件选择器 -->
<div class="file-selector mb-10px">
<el-dropdown v-if="modelData.list && modelData.list.length > 0" trigger="click">
<div class="flex items-center cursor-pointer">
<Icon icon="ep:document" class="text-danger mr-5px" />
<span>{{ currentFile?.name || '请选择文件' }}</span>
<span v-if="currentFile?.segments" class="ml-5px text-gray-500 text-12px">
({{ currentFile.segments.length }}个分片)
</span>
<Icon icon="ep:arrow-down" class="ml-5px" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(file, index) in modelData.list"
:key="index"
@click="selectFile(index)"
>
{{ file.name }}
<span v-if="file.segments" class="ml-5px text-gray-500 text-12px">
({{ file.segments.length }}个分片)
</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-else class="text-gray-400">暂无上传文件</div>
</div>
<!-- 文件内容预览 -->
<div class="file-preview bg-gray-50 p-15px rounded-md max-h-600px overflow-y-auto">
<div v-if="splitLoading" class="flex justify-center items-center py-20px">
<Icon icon="ep:loading" class="is-loading" />
<span class="ml-10px">正在加载分段内容...</span>
</div>
<template
v-else-if="currentFile && currentFile.segments && currentFile.segments.length > 0"
>
<div v-for="(segment, index) in currentFile.segments" :key="index" class="mb-10px">
<div class="text-gray-500 text-12px mb-5px">
分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
{{ segment.tokens || 0 }} Token
</div>
<div class="bg-white p-10px rounded-md">{{ segment.content }}</div>
</div>
</template>
<el-empty v-else description="暂无预览内容" />
</div>
</div>
<!-- 添加底部按钮 -->
<div class="mt-20px flex justify-between">
<div>
<el-button v-if="!modelData.id" @click="handlePrevStep">上一步</el-button>
</div>
<div>
<el-button type="primary" :loading="submitLoading" @click="handleSave">
保存并处理
</el-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, getCurrentInstance, inject, onMounted, PropType, ref } from 'vue'
import { Icon } from '@/components/Icon'
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
import { useMessage } from '@/hooks/web/useMessage'
import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
const props = defineProps({
modelValue: {
type: Object as PropType<any>,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const message = useMessage() // 消息提示
const parent = inject('parent', null) // 获取父组件实例
const modelData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
}) // 表单数据
const splitLoading = ref(false) // 分段加载状态
const currentFile = ref<any>(null) // 当前选中的文件
const submitLoading = ref(false) // 提交按钮加载状态
/** 选择文件 */
const selectFile = async (index: number) => {
currentFile.value = modelData.value.list[index]
await splitContent(currentFile.value)
}
/** 获取文件分段内容 */
const splitContent = async (file: any) => {
if (!file || !file.url) {
message.warning('文件 URL 不存在')
return
}
splitLoading.value = true
try {
// 调用后端分段接口,获取文档的分段内容、字符数和 Token 数
file.segments = await KnowledgeSegmentApi.splitContent(
file.url,
modelData.value.segmentMaxTokens
)
} catch (error) {
console.error('获取分段内容失败:', file, error)
} finally {
splitLoading.value = false
}
}
/** 处理预览分段 */
const handleAutoSegment = async () => {
// 如果没有选中文件,默认选中第一个
if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
currentFile.value = modelData.value.list[0]
}
// 如果没有选中文件,提示请先选择文件
if (!currentFile.value) {
message.warning('请先选择文件')
return
}
// 获取分段内容
await splitContent(currentFile.value)
}
/** 上一步按钮处理 */
const handlePrevStep = () => {
const parentEl = parent || getCurrentInstance()?.parent
if (parentEl && typeof parentEl.exposed?.goToPrevStep === 'function') {
parentEl.exposed.goToPrevStep()
}
}
/** 保存操作 */
const handleSave = async () => {
// 保存前验证
if (!currentFile?.value?.segments || currentFile.value.segments.length === 0) {
message.warning('请先预览分段内容')
return
}
// 设置按钮加载状态
submitLoading.value = true
try {
if (modelData.value.id) {
// 修改场景
await KnowledgeDocumentApi.updateKnowledgeDocument({
id: modelData.value.id,
segmentMaxTokens: modelData.value.segmentMaxTokens
})
} else {
// 新增场景
const data = await KnowledgeDocumentApi.createKnowledgeDocumentList({
knowledgeId: modelData.value.knowledgeId,
segmentMaxTokens: modelData.value.segmentMaxTokens,
list: modelData.value.list.map((item: any) => ({
name: item.name,
url: item.url
}))
})
modelData.value.list.forEach((document: any, index: number) => {
document.id = data[index]
})
}
// 进入下一步
const parentEl = parent || getCurrentInstance()?.parent
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
parentEl.exposed.goToNextStep()
}
} catch (error: any) {
console.error('保存失败:', modelData.value, error)
} finally {
// 关闭按钮加载状态
submitLoading.value = false
}
}
/** 初始化 */
onMounted(async () => {
// 确保 segmentMaxTokens 存在
if (!modelData.value.segmentMaxTokens) {
modelData.value.segmentMaxTokens = 500
}
// 如果没有选中文件,默认选中第一个
if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
currentFile.value = modelData.value.list[0]
}
// 如果有选中的文件,获取分段内容
if (currentFile.value) {
await splitContent(currentFile.value)
}
})
</script>

View File

@@ -0,0 +1,273 @@
<template>
<el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
<el-form-item class="mb-20px">
<div class="w-full">
<div
class="w-full border-2 border-dashed border-[#dcdfe6] rounded-md p-20px text-center hover:border-[#409eff]"
>
<el-upload
ref="uploadRef"
class="upload-demo"
drag
:action="uploadUrl"
:auto-upload="true"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:before-upload="beforeUpload"
:http-request="httpRequest"
:file-list="fileList"
:multiple="true"
:show-file-list="false"
:accept="acceptedFileTypes"
>
<div class="flex flex-col items-center justify-center py-20px">
<Icon icon="ep:upload-filled" class="text-[48px] text-[#c0c4cc] mb-10px" />
<div class="el-upload__text text-[16px] text-[#606266]">
拖拽文件至此或者
<em class="text-[#409eff] not-italic cursor-pointer">选择文件</em>
</div>
<div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
已支持 {{ supportedFileTypes.join('、') }}每个文件不超过 {{ maxFileSize }} MB
</div>
</div>
</el-upload>
</div>
<div
v-if="modelData.list && modelData.list.length > 0"
class="mt-15px grid grid-cols-1 gap-2"
>
<div
v-for="(file, index) in modelData.list"
:key="index"
class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
>
<div class="flex items-center">
<Icon icon="ep:document" class="mr-8px text-[#409eff]" />
<span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
</div>
<el-button type="danger" link @click="removeFile(index)" class="ml-2">
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
</div>
</el-form-item>
<!-- 添加下一步按钮 -->
<el-form-item>
<div class="flex justify-end w-full">
<el-button type="primary" @click="handleNextStep" :disabled="!isAllUploaded">
下一步
</el-button>
</div>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { generateAcceptedFileTypes } from '@/utils'
import { Icon } from '@/components/Icon'
const props = defineProps({
modelValue: {
type: Object as PropType<any>,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const formRef = ref() // 表单引用
const uploadRef = ref() // 上传组件引用
const parent = inject('parent', null) // 获取父组件实例
const { uploadUrl, httpRequest } = useUpload() // 使用上传组件的钩子
const message = useMessage() // 消息弹窗
const fileList = ref([]) // 文件列表
const uploadingCount = ref(0) // 上传中的文件数量
// 支持的文件类型和大小限制
const supportedFileTypes = [
'TXT',
'MARKDOWN',
'MDX',
'PDF',
'HTML',
'XLSX',
'XLS',
'DOC',
'DOCX',
'CSV',
'EML',
'MSG',
'PPTX',
'XML',
'EPUB',
'PPT',
'MD',
'HTM'
]
const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase()) // 小写的扩展名列表
const maxFileSize = 15 // 最大文件大小(MB)
// 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
/** 表单数据 */
const modelData = computed({
get: () => {
return props.modelValue
},
set: (val) => emit('update:modelValue', val)
})
/** 确保 list 属性存在 */
const ensureListExists = () => {
if (!props.modelValue.list) {
emit('update:modelValue', {
...props.modelValue,
list: []
})
}
}
/** 是否所有文件都已上传完成 */
const isAllUploaded = computed(() => {
return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
})
/**
* 上传前检查文件类型和大小
*
* @param file 待上传的文件
* @returns 是否允许上传
*/
const beforeUpload = (file) => {
// 1.1 检查文件扩展名
const fileName = file.name.toLowerCase()
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
if (!allowedExtensions.includes(fileExtension)) {
message.error('不支持的文件类型!')
return false
}
// 1.2 检查文件大小
if (!(file.size / 1024 / 1024 < maxFileSize)) {
message.error(`文件大小不能超过 ${maxFileSize} MB`)
return false
}
// 2. 增加上传中的文件计数
uploadingCount.value++
return true
}
/**
* 文件上传成功处理
*
* @param response 上传响应
* @param file 上传的文件
*/
const handleUploadSuccess = (response, file) => {
// 添加到文件列表
if (response && response.data) {
ensureListExists()
emit('update:modelValue', {
...props.modelValue,
list: [
...props.modelValue.list,
{
name: file.name,
url: response.data
}
]
})
} else {
message.error(`文件 ${file.name} 上传失败`)
}
// 减少上传中的文件计数
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
}
/**
* 文件上传失败处理
*
* @param error 错误信息
* @param file 上传的文件
*/
const handleUploadError = (error, file) => {
message.error(`文件 ${file.name} 上传失败: ${error}`)
// 减少上传中的文件计数
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
}
/**
* 文件变更处理
*
* @param file 变更的文件
*/
const handleFileChange = (file) => {
if (file.status === 'success' || file.status === 'fail') {
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
}
}
/**
* 文件移除处理
*
* @param file 被移除的文件
*/
const handleFileRemove = (file) => {
if (file.status === 'uploading') {
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
}
}
/**
* 从列表中移除文件
*
* @param index 要移除的文件索引
*/
const removeFile = (index: number) => {
// 从列表中移除文件
const newList = [...props.modelValue.list]
newList.splice(index, 1)
// 更新表单数据
emit('update:modelValue', {
...props.modelValue,
list: newList
})
}
/** 下一步按钮处理 */
const handleNextStep = () => {
// 1.1 检查是否有文件上传
if (!modelData.value.list || modelData.value.list.length === 0) {
message.warning('请上传至少一个文件')
return
}
// 1.2 检查是否有文件正在上传
if (uploadingCount.value > 0) {
message.warning('请等待所有文件上传完成')
return
}
// 2. 获取父组件的goToNextStep方法
const parentEl = parent || getCurrentInstance()?.parent
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
parentEl.exposed.goToNextStep()
}
}
/** 初始化 */
onMounted(() => {
ensureListExists()
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,193 @@
<template>
<ContentWrap>
<div class="mx-auto">
<!-- 头部导航栏 -->
<div
class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
>
<!-- 左侧标题 -->
<div class="w-200px flex items-center overflow-hidden">
<Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
<span class="ml-10px text-16px truncate">
{{ formData.id ? '编辑知识库文档' : '创建知识库文档' }}
</span>
</div>
<!-- 步骤条 -->
<div class="flex-1 flex items-center justify-center h-full">
<div class="w-400px flex items-center justify-between h-full">
<div
v-for="(step, index) in steps"
:key="index"
class="flex items-center mx-15px relative h-full"
:class="[
currentStep === index
? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
: 'text-gray-500'
]"
>
<div
class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
:class="[
currentStep === index
? 'bg-[#3473ff] text-white border-[#3473ff]'
: 'border-gray-300 bg-white text-gray-500'
]"
>
{{ index + 1 }}
</div>
<span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
</div>
</div>
</div>
<!-- 右侧按钮 - 已移除 -->
<div class="w-200px flex items-center justify-end gap-2"> </div>
</div>
<!-- 主体内容 -->
<div class="mt-50px">
<!-- 第一步上传文档 -->
<div v-if="currentStep === 0" class="mx-auto w-560px">
<UploadStep v-model="formData" ref="uploadDocumentRef" />
</div>
<!-- 第二步文档分段 -->
<div v-if="currentStep === 1" class="mx-auto w-560px">
<SplitStep v-model="formData" ref="documentSegmentRef" />
</div>
<!-- 第三步处理并完成 -->
<div v-if="currentStep === 2" class="mx-auto w-560px">
<ProcessStep v-model="formData" ref="processCompleteRef" />
</div>
</div>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
import { useRoute, useRouter } from 'vue-router'
import { useTagsViewStore } from '@/store/modules/tagsView'
import UploadStep from './UploadStep.vue'
import SplitStep from './SplitStep.vue'
import ProcessStep from './ProcessStep.vue'
import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
const { delView } = useTagsViewStore() // 视图操作
const route = useRoute() // 路由
const router = useRouter() // 路由
// 组件引用
const uploadDocumentRef = ref()
const documentSegmentRef = ref()
const processCompleteRef = ref()
const currentStep = ref(0) // 步骤控制
const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: '处理并完成' }]
const formData = ref({
knowledgeId: undefined, // 知识库编号
id: undefined, // 编辑的文档编号(documentId)
segmentMaxTokens: 500, // 分段最大 token 数
list: [] as Array<{
id: number // 文档编号
name: string // 文档名称
url: string // 文档 URL
segments: Array<{
content?: string
contentLength?: number
tokens?: number
}>
count?: number // 段落数量
process?: number // 处理进度
}> // 用于存储上传的文件列表
}) // 表单数据
provide('parent', getCurrentInstance()) // 提供 parent 给子组件使用
/** 初始化数据 */
const initData = async () => {
// 【新增场景】从路由参数中获取知识库 ID
if (route.query.knowledgeId) {
formData.value.knowledgeId = route.query.knowledgeId as any
}
// 【修改场景】从路由参数中获取文档 ID
const documentId = route.query.id
if (documentId) {
// 获取文档信息
formData.value.id = documentId as any
const document = await KnowledgeDocumentApi.getKnowledgeDocument(documentId as any)
formData.value.segmentMaxTokens = document.segmentMaxTokens
formData.value.list = [
{
id: document.id,
name: document.name,
url: document.url,
segments: []
}
]
// 进入下一步
goToNextStep()
}
}
/** 切换到下一步 */
const goToNextStep = () => {
if (currentStep.value < steps.length - 1) {
currentStep.value++
}
}
/** 切换到上一步 */
const goToPrevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
/** 返回列表页 */
const handleBack = () => {
// 先删除当前页签
delView(unref(router.currentRoute))
// 跳转到列表页
router.push({ name: 'AiKnowledgeDocument', query: { knowledgeId: formData.value.knowledgeId } })
}
/** 初始化 */
onMounted(async () => {
await initData()
})
/** 添加组件卸载前的清理代码 */
onBeforeUnmount(() => {
// 清理所有的引用
uploadDocumentRef.value = null
documentSegmentRef.value = null
processCompleteRef.value = null
})
/** 暴露方法给子组件使用 */
defineExpose({
goToNextStep,
goToPrevStep,
handleBack
})
</script>
<style lang="scss" scoped>
.border-bottom {
border-bottom: 1px solid #dcdfe6;
}
.text-primary {
color: #3473ff;
}
.bg-primary {
background-color: #3473ff;
}
.border-primary {
border-color: #3473ff;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="文件名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入文件名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="是否启用" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择是否启用"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="handleCreate" v-hasPermi="['ai:knowledge:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="文档编号" align="center" prop="id" />
<el-table-column label="文件名称" align="center" prop="name" />
<el-table-column label="字符数" align="center" prop="contentLength" />
<el-table-column label="Token 数" align="center" prop="tokens" />
<el-table-column label="分片最大 Token 数" align="center" prop="segmentMaxTokens" />
<el-table-column label="召回次数" align="center" prop="retrievalCount" />
<el-table-column label="是否启用" align="center" prop="status">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(scope.row)"
:disabled="!checkPermi(['ai:knowledge:update'])"
/>
</template>
</el-table-column>
<el-table-column
label="上传时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="handleUpdate(scope.row.id)"
v-hasPermi="['ai:knowledge:update']"
>
编辑
</el-button>
<el-button
link
type="primary"
@click="handleSegment(scope.row.id)"
v-hasPermi="['ai:knowledge:query']"
>
分段
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['ai:knowledge:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<!-- <KnowledgeDocumentForm ref="formRef" @success="getList" /> -->
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { KnowledgeDocumentApi, KnowledgeDocumentVO } from '@/api/ai/knowledge/document'
import { useRoute, useRouter } from 'vue-router'
import { checkPermi } from '@/utils/permission'
import { CommonStatusEnum } from '@/utils/constants'
// import KnowledgeDocumentForm from './KnowledgeDocumentForm.vue'
/** AI 知识库文档 列表 */
defineOptions({ name: 'KnowledgeDocument' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const route = useRoute() // 路由
const router = useRouter() // 路由
const loading = ref(true) // 列表的加载中
const list = ref<KnowledgeDocumentVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
knowledgeId: undefined
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await KnowledgeDocumentApi.getKnowledgeDocumentPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 跳转到创建文档页面 */
const handleCreate = () => {
router.push({
name: 'AiKnowledgeDocumentCreate',
query: { knowledgeId: queryParams.knowledgeId }
})
}
/** 跳转到更新文档页面 */
const handleUpdate = (id: number) => {
router.push({
name: 'AiKnowledgeDocumentUpdate',
query: { id, knowledgeId: queryParams.knowledgeId }
})
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await KnowledgeDocumentApi.deleteKnowledgeDocument(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 修改状态操作 */
const handleStatusChange = async (row: KnowledgeDocumentVO) => {
try {
// 修改状态的二次确认
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '禁用'
await message.confirm('确认要"' + text + '""' + row.name + '"文档吗?')
// 发起修改状态
await KnowledgeDocumentApi.updateKnowledgeDocumentStatus({ id: row.id, status: row.status })
message.success(t('common.updateSuccess'))
// 刷新列表
await getList()
} catch {
// 取消后,进行恢复按钮
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
}
}
/** 跳转到知识库分段页面 */
const handleSegment = (id: number) => {
router.push({
name: 'AiKnowledgeSegment',
query: { documentId: id }
})
}
/** 初始化 **/
onMounted(() => {
// 如果知识库 ID 不存在,显示错误提示并关闭页面
if (!route.query.knowledgeId) {
message.error('知识库 ID 不存在,无法查看文档列表')
// 关闭当前路由,返回到知识库列表页面
router.push({ name: 'AiKnowledge' })
return
}
// 从路由参数中获取知识库 ID
queryParams.knowledgeId = route.query.knowledgeId as any
getList()
})
</script>