【新增】前端代码第一次提交
This commit is contained in:
146
src/views/ai/knowledge/document/form/ProcessStep.vue
Normal file
146
src/views/ai/knowledge/document/form/ProcessStep.vue
Normal 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>
|
||||
238
src/views/ai/knowledge/document/form/SplitStep.vue
Normal file
238
src/views/ai/knowledge/document/form/SplitStep.vue
Normal 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>
|
||||
273
src/views/ai/knowledge/document/form/UploadStep.vue
Normal file
273
src/views/ai/knowledge/document/form/UploadStep.vue
Normal 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>
|
||||
193
src/views/ai/knowledge/document/form/index.vue
Normal file
193
src/views/ai/knowledge/document/form/index.vue
Normal 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>
|
||||
236
src/views/ai/knowledge/document/index.vue
Normal file
236
src/views/ai/knowledge/document/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user