【功能新增】AI:知识库文档上传:10% 搭建整体页面结构
							parent
							
								
									94091598a3
								
							
						
					
					
						commit
						b7d7b11d31
					
				|  | @ -630,6 +630,18 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|           icon: 'ep:document', | ||||
|           noCache: false | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         path: 'console/knowledge/document/create', | ||||
|         component: () => import('@/views/ai/knowledge/document/create/index.vue'), | ||||
|         name: 'AiKnowledgeDocumentCreate', | ||||
|         meta: { | ||||
|           title: '创建文档', | ||||
|           icon: 'ep:plus', | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|           activeMenu: '/ai/console/knowledge/document' | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|  |  | |||
|  | @ -0,0 +1,235 @@ | |||
| <template> | ||||
|   <div class="process-complete"> | ||||
|     <div class="mb-20px"> | ||||
|       <el-alert | ||||
|         title="处理说明" | ||||
|         type="info" | ||||
|         description="系统将对文档进行处理,包括文本提取、向量化等操作,处理完成后文档将被添加到知识库中。" | ||||
|         show-icon | ||||
|         :closable="false" | ||||
|       /> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="mb-20px"> | ||||
|       <el-card class="box-card"> | ||||
|         <template #header> | ||||
|           <div class="card-header"> | ||||
|             <span class="text-16px font-bold">文档信息</span> | ||||
|           </div> | ||||
|         </template> | ||||
|         <div class="document-info"> | ||||
|           <div class="info-item"> | ||||
|             <span class="label">文档名称:</span> | ||||
|             <span class="value">{{ modelData.name }}</span> | ||||
|           </div> | ||||
|           <div class="info-item"> | ||||
|             <span class="label">知识库:</span> | ||||
|             <span class="value">{{ getKnowledgeBaseName(modelData.knowledgeBaseId) }}</span> | ||||
|           </div> | ||||
|           <div class="info-item"> | ||||
|             <span class="label">文档类型:</span> | ||||
|             <span class="value">{{ getDocumentTypeName(modelData.documentType) }}</span> | ||||
|           </div> | ||||
|           <div class="info-item"> | ||||
|             <span class="label">段落数量:</span> | ||||
|             <span class="value">{{ modelData.segments.length }}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|       </el-card> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="mb-20px"> | ||||
|       <el-card class="box-card"> | ||||
|         <template #header> | ||||
|           <div class="card-header"> | ||||
|             <span class="text-16px font-bold">处理选项</span> | ||||
|           </div> | ||||
|         </template> | ||||
|         <div class="process-options"> | ||||
|           <el-form :model="processOptions" label-width="120px"> | ||||
|             <el-form-item label="处理模式"> | ||||
|               <el-radio-group v-model="processOptions.mode"> | ||||
|                 <el-radio :label="1">标准处理</el-radio> | ||||
|                 <el-radio :label="2">高级处理</el-radio> | ||||
|               </el-radio-group> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="向量模型" v-if="processOptions.mode === 2"> | ||||
|               <el-select v-model="processOptions.vectorModel" placeholder="请选择向量模型"> | ||||
|                 <el-option label="文本嵌入模型-基础版" value="text-embedding-basic" /> | ||||
|                 <el-option label="文本嵌入模型-高级版" value="text-embedding-advanced" /> | ||||
|                 <el-option label="多模态嵌入模型" value="multimodal-embedding" /> | ||||
|               </el-select> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="处理优先级" v-if="processOptions.mode === 2"> | ||||
|               <el-select v-model="processOptions.priority" placeholder="请选择处理优先级"> | ||||
|                 <el-option label="低" value="low" /> | ||||
|                 <el-option label="中" value="medium" /> | ||||
|                 <el-option label="高" value="high" /> | ||||
|               </el-select> | ||||
|             </el-form-item> | ||||
|           </el-form> | ||||
|         </div> | ||||
|       </el-card> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="mb-20px"> | ||||
|       <el-card class="box-card"> | ||||
|         <template #header> | ||||
|           <div class="card-header"> | ||||
|             <span class="text-16px font-bold">处理状态</span> | ||||
|           </div> | ||||
|         </template> | ||||
|         <div class="process-status"> | ||||
|           <div v-if="!isProcessing && !isProcessed"> | ||||
|             <el-empty description="尚未开始处理" /> | ||||
|             <div class="flex justify-center mt-20px"> | ||||
|               <el-button type="primary" @click="handleStartProcess">开始处理</el-button> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div v-else-if="isProcessing"> | ||||
|             <div class="flex flex-col items-center"> | ||||
|               <el-progress type="circle" :percentage="processPercentage" /> | ||||
|               <div class="mt-10px">{{ processStatus }}</div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div v-else> | ||||
|             <div class="flex items-center justify-center"> | ||||
|               <el-result icon="success" title="处理完成" sub-title="文档已成功处理并添加到知识库中"> | ||||
|                 <template #extra> | ||||
|                   <el-button type="primary" @click="handleViewDocument">查看文档</el-button> | ||||
|                 </template> | ||||
|               </el-result> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </el-card> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { PropType } from 'vue' | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   modelValue: { | ||||
|     type: Object as PropType<any>, | ||||
|     required: true | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| 
 | ||||
| // 表单数据 | ||||
| const modelData = computed({ | ||||
|   get: () => props.modelValue, | ||||
|   set: (val) => emit('update:modelValue', val) | ||||
| }) | ||||
| 
 | ||||
| // 处理选项 | ||||
| const processOptions = ref({ | ||||
|   mode: 1, // 1: 标准处理, 2: 高级处理 | ||||
|   vectorModel: 'text-embedding-basic', | ||||
|   priority: 'medium' | ||||
| }) | ||||
| 
 | ||||
| // 处理状态 | ||||
| const isProcessing = ref(false) | ||||
| const isProcessed = ref(false) | ||||
| const processPercentage = ref(0) | ||||
| const processStatus = ref('正在准备处理...') | ||||
| 
 | ||||
| // 知识库列表(模拟数据) | ||||
| const knowledgeBaseList = [ | ||||
|   { id: 1, name: '产品知识库' }, | ||||
|   { id: 2, name: '技术文档库' }, | ||||
|   { id: 3, name: '客户服务知识库' } | ||||
| ] | ||||
| 
 | ||||
| // 获取知识库名称 | ||||
| const getKnowledgeBaseName = (id) => { | ||||
|   const base = knowledgeBaseList.find((item) => item.id === id) | ||||
|   return base ? base.name : '未知知识库' | ||||
| } | ||||
| 
 | ||||
| // 获取文档类型名称 | ||||
| const getDocumentTypeName = (type) => { | ||||
|   const typeMap = { | ||||
|     pdf: 'PDF文档', | ||||
|     word: 'Word文档', | ||||
|     text: '文本文件', | ||||
|     url: '网页链接' | ||||
|   } | ||||
|   return typeMap[type] || '未知类型' | ||||
| } | ||||
| 
 | ||||
| // 开始处理 | ||||
| const handleStartProcess = () => { | ||||
|   isProcessing.value = true | ||||
|   processPercentage.value = 0 | ||||
|   processStatus.value = '正在准备处理...' | ||||
| 
 | ||||
|   // 模拟处理过程 | ||||
|   const timer = setInterval(() => { | ||||
|     processPercentage.value += 10 | ||||
| 
 | ||||
|     if (processPercentage.value < 30) { | ||||
|       processStatus.value = '正在提取文本内容...' | ||||
|     } else if (processPercentage.value < 60) { | ||||
|       processStatus.value = '正在进行向量化处理...' | ||||
|     } else if (processPercentage.value < 90) { | ||||
|       processStatus.value = '正在写入知识库...' | ||||
|     } else { | ||||
|       processStatus.value = '处理完成,正在整理结果...' | ||||
|     } | ||||
| 
 | ||||
|     if (processPercentage.value >= 100) { | ||||
|       clearInterval(timer) | ||||
|       isProcessing.value = false | ||||
|       isProcessed.value = true | ||||
|       modelData.value.status = 2 // 已完成 | ||||
|     } | ||||
|   }, 500) | ||||
| } | ||||
| 
 | ||||
| // 查看文档 | ||||
| const handleViewDocument = () => { | ||||
|   // 跳转到文档详情页 | ||||
|   console.log('查看文档:', modelData.value.id) | ||||
| } | ||||
| 
 | ||||
| // 表单校验 | ||||
| const validate = () => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     if (modelData.value.status === 2) { | ||||
|       resolve(true) | ||||
|     } else { | ||||
|       reject(new Error('请先完成文档处理')) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 对外暴露方法 | ||||
| defineExpose({ | ||||
|   validate | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .process-complete { | ||||
|   .document-info { | ||||
|     .info-item { | ||||
|       margin-bottom: 10px; | ||||
|       display: flex; | ||||
| 
 | ||||
|       .label { | ||||
|         width: 100px; | ||||
|         color: #606266; | ||||
|       } | ||||
| 
 | ||||
|       .value { | ||||
|         font-weight: bold; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,234 @@ | |||
| <template> | ||||
|   <div class="document-segment"> | ||||
|     <div class="mb-20px"> | ||||
|       <el-alert | ||||
|         title="文档分段说明" | ||||
|         type="info" | ||||
|         description="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。" | ||||
|         show-icon | ||||
|         :closable="false" | ||||
|       /> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="mb-20px flex justify-between items-center"> | ||||
|       <div class="text-16px font-bold">分段设置</div> | ||||
|       <div> | ||||
|         <el-button type="primary" @click="handleAutoSegment">自动分段</el-button> | ||||
|         <el-button @click="handleAddSegment">添加段落</el-button> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="segment-settings mb-20px"> | ||||
|       <el-form :model="segmentSettings" label-width="120px"> | ||||
|         <el-form-item label="分段方式"> | ||||
|           <el-radio-group v-model="segmentSettings.type"> | ||||
|             <el-radio :label="1">按段落分割</el-radio> | ||||
|             <el-radio :label="2">按字数分割</el-radio> | ||||
|             <el-radio :label="3">按标题分割</el-radio> | ||||
|           </el-radio-group> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="最大字数" v-if="segmentSettings.type === 2"> | ||||
|           <el-input-number v-model="segmentSettings.maxChars" :min="100" :max="5000" /> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="segment-list"> | ||||
|       <div class="text-16px font-bold mb-10px">段落列表 ({{ modelData.segments.length }})</div> | ||||
| 
 | ||||
|       <el-empty v-if="modelData.segments.length === 0" description="暂无段落数据" /> | ||||
| 
 | ||||
|       <div v-else> | ||||
|         <el-collapse v-model="activeSegments"> | ||||
|           <el-collapse-item | ||||
|             v-for="(segment, index) in modelData.segments" | ||||
|             :key="index" | ||||
|             :title="`段落 ${index + 1}`" | ||||
|             :name="index" | ||||
|           > | ||||
|             <div class="segment-content"> | ||||
|               <el-input | ||||
|                 v-model="segment.content" | ||||
|                 type="textarea" | ||||
|                 :rows="5" | ||||
|                 placeholder="段落内容" | ||||
|               /> | ||||
|               <div class="mt-10px flex justify-end"> | ||||
|                 <el-button type="danger" size="small" @click="handleDeleteSegment(index)"> | ||||
|                   删除段落 | ||||
|                 </el-button> | ||||
|               </div> | ||||
|             </div> | ||||
|           </el-collapse-item> | ||||
|         </el-collapse> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- 添加底部按钮 --> | ||||
|     <div class="mt-20px flex justify-between"> | ||||
|       <el-button @click="handlePrevStep">上一步</el-button> | ||||
|       <el-button type="primary" @click="handleNextStep">保存并处理</el-button> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { PropType } from 'vue' | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   modelValue: { | ||||
|     type: Object as PropType<any>, | ||||
|     required: true | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| 
 | ||||
| // 获取父组件实例 | ||||
| const parent = inject('parent', null) | ||||
| 
 | ||||
| // 表单数据 | ||||
| const modelData = computed({ | ||||
|   get: () => props.modelValue, | ||||
|   set: (val) => emit('update:modelValue', val) | ||||
| }) | ||||
| 
 | ||||
| // 分段设置 | ||||
| const segmentSettings = ref({ | ||||
|   type: 1, // 1: 按段落, 2: 按字数, 3: 按标题 | ||||
|   maxChars: 1000 | ||||
| }) | ||||
| 
 | ||||
| // 当前展开的段落 | ||||
| const activeSegments = ref([0]) | ||||
| 
 | ||||
| // 自动分段 | ||||
| const handleAutoSegment = () => { | ||||
|   // 根据文档类型和分段设置进行自动分段 | ||||
|   // 这里只是模拟实现,实际需要根据文档内容进行分析 | ||||
| 
 | ||||
|   // 清空现有段落 | ||||
|   modelData.value.segments = [] | ||||
| 
 | ||||
|   // 模拟生成段落 | ||||
|   if (modelData.value.documentType === 'text' && modelData.value.content) { | ||||
|     // 文本类型,直接按段落或字数分割 | ||||
|     const content = modelData.value.content | ||||
| 
 | ||||
|     if (segmentSettings.value.type === 1) { | ||||
|       // 按段落分割 | ||||
|       const paragraphs = content.split(/\n\s*\n/) | ||||
|       paragraphs.forEach((paragraph) => { | ||||
|         if (paragraph.trim()) { | ||||
|           modelData.value.segments.push({ | ||||
|             content: paragraph.trim(), | ||||
|             order: modelData.value.segments.length + 1 | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     } else if (segmentSettings.value.type === 2) { | ||||
|       // 按字数分割 | ||||
|       const maxChars = segmentSettings.value.maxChars | ||||
|       let remaining = content | ||||
| 
 | ||||
|       while (remaining.length > 0) { | ||||
|         const segment = remaining.substring(0, maxChars) | ||||
|         remaining = remaining.substring(maxChars) | ||||
| 
 | ||||
|         modelData.value.segments.push({ | ||||
|           content: segment, | ||||
|           order: modelData.value.segments.length + 1 | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|   } else { | ||||
|     // 其他类型文档,模拟生成5个段落 | ||||
|     for (let i = 0; i < 5; i++) { | ||||
|       modelData.value.segments.push({ | ||||
|         content: `这是第 ${i + 1} 个自动生成的段落,实际内容将根据文档解析结果填充。`, | ||||
|         order: i + 1 | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // 默认展开第一个段落 | ||||
|   activeSegments.value = [0] | ||||
| } | ||||
| 
 | ||||
| // 添加段落 | ||||
| const handleAddSegment = () => { | ||||
|   modelData.value.segments.push({ | ||||
|     content: '', | ||||
|     order: modelData.value.segments.length + 1 | ||||
|   }) | ||||
| 
 | ||||
|   // 展开新添加的段落 | ||||
|   activeSegments.value = [modelData.value.segments.length - 1] | ||||
| } | ||||
| 
 | ||||
| // 删除段落 | ||||
| const handleDeleteSegment = (index) => { | ||||
|   modelData.value.segments.splice(index, 1) | ||||
| 
 | ||||
|   // 更新段落顺序 | ||||
|   modelData.value.segments.forEach((segment, idx) => { | ||||
|     segment.order = idx + 1 | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 上一步按钮处理 | ||||
| const handlePrevStep = () => { | ||||
|   // 获取父组件的goToPrevStep方法 | ||||
|   const parentEl = parent || getCurrentInstance()?.parent | ||||
|   if (parentEl && typeof parentEl.exposed?.goToPrevStep === 'function') { | ||||
|     parentEl.exposed.goToPrevStep() | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 下一步按钮处理 | ||||
| const handleNextStep = () => { | ||||
|   // 获取父组件的goToNextStep方法 | ||||
|   const parentEl = parent || getCurrentInstance()?.parent | ||||
|   if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') { | ||||
|     parentEl.exposed.goToNextStep() | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 表单校验 | ||||
| const validate = () => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     if (modelData.value.segments.length === 0) { | ||||
|       reject(new Error('请至少添加一个段落')) | ||||
|     } else { | ||||
|       // 检查是否有空段落 | ||||
|       const emptySegment = modelData.value.segments.find((segment) => !segment.content.trim()) | ||||
|       if (emptySegment) { | ||||
|         reject(new Error('存在空段落,请填写内容或删除')) | ||||
|       } else { | ||||
|         resolve(true) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 对外暴露方法 | ||||
| defineExpose({ | ||||
|   validate | ||||
| }) | ||||
| 
 | ||||
| // 初始化 | ||||
| onMounted(() => { | ||||
|   // 如果已有段落数据,默认展开第一个 | ||||
|   if (modelData.value.segments && modelData.value.segments.length > 0) { | ||||
|     activeSegments.value = [0] | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .document-segment { | ||||
|   .segment-content { | ||||
|     padding: 10px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,225 @@ | |||
| <template> | ||||
|   <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> | ||||
|     <el-form-item label="文档名称" prop="name" class="mb-20px"> | ||||
|       <el-input v-model="modelData.name" clearable placeholder="请输入文档名称" /> | ||||
|     </el-form-item> | ||||
|     <el-form-item label="知识库" prop="knowledgeBaseId" class="mb-20px"> | ||||
|       <el-select | ||||
|         class="!w-full" | ||||
|         v-model="modelData.knowledgeBaseId" | ||||
|         clearable | ||||
|         placeholder="请选择知识库" | ||||
|       > | ||||
|         <el-option | ||||
|           v-for="base in knowledgeBaseList" | ||||
|           :key="base.id" | ||||
|           :label="base.name" | ||||
|           :value="base.id" | ||||
|         /> | ||||
|       </el-select> | ||||
|     </el-form-item> | ||||
|     <el-form-item label="文档类型" prop="documentType" class="mb-20px"> | ||||
|       <el-select | ||||
|         class="!w-full" | ||||
|         v-model="modelData.documentType" | ||||
|         clearable | ||||
|         placeholder="请选择文档类型" | ||||
|       > | ||||
|         <el-option label="PDF文档" value="pdf" /> | ||||
|         <el-option label="Word文档" value="word" /> | ||||
|         <el-option label="文本文件" value="text" /> | ||||
|         <el-option label="网页链接" value="url" /> | ||||
|       </el-select> | ||||
|     </el-form-item> | ||||
|     <el-form-item | ||||
|       label="文档内容" | ||||
|       prop="content" | ||||
|       class="mb-20px" | ||||
|       v-if="modelData.documentType === 'text'" | ||||
|     > | ||||
|       <el-input | ||||
|         v-model="modelData.content" | ||||
|         type="textarea" | ||||
|         :rows="6" | ||||
|         placeholder="请输入文档内容" | ||||
|       /> | ||||
|     </el-form-item> | ||||
|     <el-form-item | ||||
|       label="网页链接" | ||||
|       prop="url" | ||||
|       class="mb-20px" | ||||
|       v-if="modelData.documentType === 'url'" | ||||
|     > | ||||
|       <el-input v-model="modelData.url" clearable placeholder="请输入网页链接" /> | ||||
|     </el-form-item> | ||||
|     <el-form-item | ||||
|       label="上传文件" | ||||
|       prop="file" | ||||
|       class="mb-20px" | ||||
|       v-if="['pdf', 'word'].includes(modelData.documentType)" | ||||
|     > | ||||
|       <el-upload | ||||
|         class="upload-demo" | ||||
|         drag | ||||
|         action="#" | ||||
|         :auto-upload="false" | ||||
|         :on-change="handleFileChange" | ||||
|         :limit="1" | ||||
|       > | ||||
|         <el-icon class="el-icon--upload"><upload-filled /></el-icon> | ||||
|         <div class="el-upload__text"> 拖拽文件到此处,或 <em>点击上传</em> </div> | ||||
|         <template #tip> | ||||
|           <div class="el-upload__tip"> | ||||
|             {{ modelData.documentType === 'pdf' ? 'PDF文件' : 'Word文件(.docx, .doc)' }} | ||||
|           </div> | ||||
|         </template> | ||||
|       </el-upload> | ||||
|     </el-form-item> | ||||
| 
 | ||||
|     <!-- 添加下一步按钮 --> | ||||
|     <el-form-item> | ||||
|       <div class="flex justify-end"> | ||||
|         <el-button type="primary" @click="handleNextStep">下一步</el-button> | ||||
|       </div> | ||||
|     </el-form-item> | ||||
|   </el-form> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { PropType } from 'vue' | ||||
| import { UploadFilled } from '@element-plus/icons-vue' | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   modelValue: { | ||||
|     type: Object as PropType<any>, | ||||
|     required: true | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| 
 | ||||
| // 表单引用 | ||||
| const formRef = ref() | ||||
| 
 | ||||
| // 获取父组件实例 | ||||
| const parent = inject('parent', null) | ||||
| 
 | ||||
| // 表单数据 | ||||
| const modelData = computed({ | ||||
|   get: () => props.modelValue, | ||||
|   set: (val) => emit('update:modelValue', val) | ||||
| }) | ||||
| 
 | ||||
| // 知识库列表 | ||||
| interface KnowledgeBase { | ||||
|   id: number | ||||
|   name: string | ||||
| } | ||||
| 
 | ||||
| const knowledgeBaseList = ref<KnowledgeBase[]>([]) | ||||
| 
 | ||||
| // 表单校验规则 | ||||
| const rules = { | ||||
|   name: [{ required: true, message: '请输入文档名称', trigger: 'blur' }], | ||||
|   knowledgeBaseId: [{ required: true, message: '请选择知识库', trigger: 'change' }], | ||||
|   documentType: [{ required: true, message: '请选择文档类型', trigger: 'change' }], | ||||
|   content: [ | ||||
|     { | ||||
|       required: true, | ||||
|       message: '请输入文档内容', | ||||
|       trigger: 'blur', | ||||
|       validator: (rule, value, callback) => { | ||||
|         if (modelData.value.documentType === 'text' && !value) { | ||||
|           callback(new Error('请输入文档内容')) | ||||
|         } else { | ||||
|           callback() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   url: [ | ||||
|     { | ||||
|       required: true, | ||||
|       message: '请输入网页链接', | ||||
|       trigger: 'blur', | ||||
|       validator: (rule, value, callback) => { | ||||
|         if (modelData.value.documentType === 'url' && !value) { | ||||
|           callback(new Error('请输入网页链接')) | ||||
|         } else { | ||||
|           callback() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   file: [ | ||||
|     { | ||||
|       required: true, | ||||
|       message: '请上传文件', | ||||
|       trigger: 'change', | ||||
|       validator: (rule, value, callback) => { | ||||
|         if (['pdf', 'word'].includes(modelData.value.documentType) && !modelData.value.file) { | ||||
|           callback(new Error('请上传文件')) | ||||
|         } else { | ||||
|           callback() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| 
 | ||||
| // 文件上传处理 | ||||
| const handleFileChange = (file) => { | ||||
|   modelData.value.file = file.raw | ||||
| } | ||||
| 
 | ||||
| // 下一步按钮处理 | ||||
| const handleNextStep = () => { | ||||
|   // 获取父组件的goToNextStep方法 | ||||
|   const parentEl = parent || getCurrentInstance()?.parent | ||||
|   if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') { | ||||
|     parentEl.exposed.goToNextStep() | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 初始化数据 | ||||
| const initData = async () => { | ||||
|   // 获取知识库列表 | ||||
|   // knowledgeBaseList.value = await KnowledgeBaseApi.getKnowledgeBaseList() | ||||
| 
 | ||||
|   // 模拟数据 | ||||
|   knowledgeBaseList.value = [ | ||||
|     { id: 1, name: '产品知识库' }, | ||||
|     { id: 2, name: '技术文档库' }, | ||||
|     { id: 3, name: '客户服务知识库' } | ||||
|   ] | ||||
| } | ||||
| 
 | ||||
| // 表单校验 | ||||
| const validate = () => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     formRef.value?.validate((valid) => { | ||||
|       if (valid) { | ||||
|         resolve(true) | ||||
|       } else { | ||||
|         reject(new Error('请完善表单信息')) | ||||
|       } | ||||
|     }) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 对外暴露方法 | ||||
| defineExpose({ | ||||
|   validate | ||||
| }) | ||||
| 
 | ||||
| // 初始化 | ||||
| onMounted(() => { | ||||
|   initData() | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .upload-demo { | ||||
|   width: 100%; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,214 @@ | |||
| <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" :title="formData.name || '创建知识库文档'"> | ||||
|             {{ formData.name || '创建知识库文档' }} | ||||
|           </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 { useMessage } from '@/hooks/web/useMessage' | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
| import UploadStep from './UploadStep.vue' | ||||
| import SplitStep from './SplitStep.vue' | ||||
| import ProcessStep from './ProcessStep.vue' | ||||
| 
 | ||||
| const router = useRouter() | ||||
| const { delView } = useTagsViewStore() // 视图操作 | ||||
| const route = useRoute() | ||||
| const message = useMessage() | ||||
| 
 | ||||
| // 组件引用 | ||||
| const uploadDocumentRef = ref() | ||||
| const documentSegmentRef = ref() | ||||
| const processCompleteRef = ref() | ||||
| 
 | ||||
| const currentStep = ref(0) // 步骤控制 | ||||
| const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: '处理并完成' }] | ||||
| 
 | ||||
| // 表单数据 | ||||
| const formData = ref({ | ||||
|   id: undefined, | ||||
|   name: '', | ||||
|   knowledgeBaseId: undefined, | ||||
|   documentType: undefined, | ||||
|   content: '', | ||||
|   file: null, | ||||
|   segments: [], | ||||
|   status: 0 // 0: 草稿, 1: 处理中, 2: 已完成 | ||||
| }) | ||||
| 
 | ||||
| /** 初始化数据 */ | ||||
| const initData = async () => { | ||||
|   const documentId = route.params.id as string | ||||
|   if (documentId) { | ||||
|     // 修改场景 | ||||
|     // 这里需要调用API获取文档数据 | ||||
|     // formData.value = await DocumentApi.getDocument(documentId) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 切换到下一步 */ | ||||
| const goToNextStep = () => { | ||||
|   if (currentStep.value < steps.length - 1) { | ||||
|     currentStep.value++ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 切换到上一步 */ | ||||
| const goToPrevStep = () => { | ||||
|   if (currentStep.value > 0) { | ||||
|     currentStep.value-- | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 保存操作 */ | ||||
| const handleSave = async () => { | ||||
|   try { | ||||
|     // 更新表单数据 | ||||
|     const documentData = { | ||||
|       ...formData.value | ||||
|     } | ||||
| 
 | ||||
|     if (formData.value.id) { | ||||
|       // 修改场景 | ||||
|       // await DocumentApi.updateDocument(documentData) | ||||
|       message.success('修改成功') | ||||
|     } else { | ||||
|       // 新增场景 | ||||
|       // formData.value.id = await DocumentApi.createDocument(documentData) | ||||
|       message.success('新增成功') | ||||
|       try { | ||||
|         await message.confirm('创建文档成功,是否继续编辑?') | ||||
|         // 用户点击继续编辑,跳转到编辑页面 | ||||
|         await nextTick() | ||||
|         // 先删除当前页签 | ||||
|         delView(unref(router.currentRoute)) | ||||
|         // 跳转到编辑页面 | ||||
|         await router.push({ | ||||
|           name: 'AiKnowledgeDocumentUpdate', | ||||
|           params: { id: formData.value.id } | ||||
|         }) | ||||
|       } catch { | ||||
|         // 先删除当前页签 | ||||
|         delView(unref(router.currentRoute)) | ||||
|         // 用户点击返回列表 | ||||
|         await router.push({ name: 'AiKnowledgeDocument' }) | ||||
|       } | ||||
|     } | ||||
|   } catch (error: any) { | ||||
|     console.error('保存失败:', error) | ||||
|     message.warning(error.message || '请完善所有步骤的必填信息') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 返回列表页 */ | ||||
| const handleBack = () => { | ||||
|   // 先删除当前页签 | ||||
|   delView(unref(router.currentRoute)) | ||||
|   // 跳转到列表页 | ||||
|   router.push({ name: 'AiKnowledgeDocument' }) | ||||
| } | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   await initData() | ||||
| }) | ||||
| 
 | ||||
| // 提供parent给子组件使用 | ||||
| provide('parent', getCurrentInstance()) | ||||
| 
 | ||||
| // 添加组件卸载前的清理代码 | ||||
| onBeforeUnmount(() => { | ||||
|   // 清理所有的引用 | ||||
|   uploadDocumentRef.value = null | ||||
|   documentSegmentRef.value = null | ||||
|   processCompleteRef.value = null | ||||
| }) | ||||
| 
 | ||||
| // 暴露方法给子组件使用 | ||||
| defineExpose({ | ||||
|   goToNextStep, | ||||
|   goToPrevStep, | ||||
|   handleSave | ||||
| }) | ||||
| </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> | ||||
|  | @ -35,12 +35,7 @@ | |||
|       <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="openForm('create')" | ||||
|           v-hasPermi="['ai:knowledge:create']" | ||||
|         > | ||||
|         <el-button type="primary" plain @click="handleCreate" v-hasPermi="['ai:knowledge:create']"> | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> 新增 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|  | @ -106,7 +101,7 @@ | |||
| import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { KnowledgeDocumentApi, KnowledgeDocumentVO } from '@/api/ai/knowledge/document' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| // import KnowledgeDocumentForm from './KnowledgeDocumentForm.vue' | ||||
| 
 | ||||
| /** AI 知识库文档 列表 */ | ||||
|  | @ -115,6 +110,7 @@ defineOptions({ name: 'KnowledgeDocument' }) | |||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| const route = useRoute() // 路由 | ||||
| const router = useRouter() // 路由 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const list = ref<KnowledgeDocumentVO[]>([]) // 列表的数据 | ||||
|  | @ -158,6 +154,11 @@ const openForm = (type: string, id?: number) => { | |||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| /** 跳转到创建文档页面 */ | ||||
| const handleCreate = () => { | ||||
|   router.push({ name: 'AiKnowledgeDocumentCreate' }) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (id: number) => { | ||||
|   try { | ||||
|  |  | |||
|  | @ -380,7 +380,7 @@ const handleStepClick = async (index: number) => { | |||
|     if (index === 2) { | ||||
|       await nextTick() | ||||
|       // 等待更长时间确保组件完全初始化 | ||||
|       await new Promise(resolve => setTimeout(resolve, 200)) | ||||
|       await new Promise((resolve) => setTimeout(resolve, 200)) | ||||
|       if (processDesignRef.value?.refresh) { | ||||
|         await processDesignRef.value.refresh() | ||||
|       } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 YunaiV
						YunaiV