浏览代码

【功能新增】AI:知识库文档上传:10% 搭建整体页面结构

YunaiV 5 月之前
父节点
当前提交
b7d7b11d31

+ 12 - 0
src/router/modules/remaining.ts

@@ -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'
+        }
       }
     ]
   },

+ 235 - 0
src/views/ai/knowledge/document/create/ProcessStep.vue

@@ -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>

+ 234 - 0
src/views/ai/knowledge/document/create/SplitStep.vue

@@ -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>

+ 225 - 0
src/views/ai/knowledge/document/create/UploadStep.vue

@@ -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>

+ 214 - 0
src/views/ai/knowledge/document/create/index.vue

@@ -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>

+ 8 - 7
src/views/ai/knowledge/document/index.vue

@@ -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 {

+ 1 - 1
src/views/bpm/model/form/index.vue

@@ -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()
       }