Преглед изворни кода

【功能新增】AI:知识库文档上传:20%,UploadStep 基本搭建出来

YunaiV пре 5 месеци
родитељ
комит
7cd6a5d9b4

+ 58 - 0
src/utils/index.ts

@@ -116,6 +116,64 @@ export function toAnyString() {
   return str
 }
 
+/**
+ * 根据支持的文件类型生成 accept 属性值
+ * 
+ * @param supportedFileTypes 支持的文件类型数组,如 ['PDF', 'DOC', 'DOCX']
+ * @returns 用于文件上传组件 accept 属性的字符串
+ */
+export const generateAcceptedFileTypes = (supportedFileTypes: string[]): string => {
+  const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase())
+  const mimeTypes: string[] = []
+
+  // 添加常见的 MIME 类型映射
+  if (allowedExtensions.includes('txt')) {
+    mimeTypes.push('text/plain')
+  }
+  if (allowedExtensions.includes('pdf')) {
+    mimeTypes.push('application/pdf')
+  }
+  if (allowedExtensions.includes('html') || allowedExtensions.includes('htm')) {
+    mimeTypes.push('text/html')
+  }
+  if (allowedExtensions.includes('csv')) {
+    mimeTypes.push('text/csv')
+  }
+  if (allowedExtensions.includes('xlsx') || allowedExtensions.includes('xls')) {
+    mimeTypes.push('application/vnd.ms-excel')
+    mimeTypes.push('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+  }
+  if (allowedExtensions.includes('docx') || allowedExtensions.includes('doc')) {
+    mimeTypes.push('application/msword')
+    mimeTypes.push('application/vnd.openxmlformats-officedocument.wordprocessingml.document')
+  }
+  if (allowedExtensions.includes('pptx') || allowedExtensions.includes('ppt')) {
+    mimeTypes.push('application/vnd.ms-powerpoint')
+    mimeTypes.push('application/vnd.openxmlformats-officedocument.presentationml.presentation')
+  }
+  if (allowedExtensions.includes('xml')) {
+    mimeTypes.push('application/xml')
+    mimeTypes.push('text/xml')
+  }
+  if (allowedExtensions.includes('md') || allowedExtensions.includes('markdown')) {
+    mimeTypes.push('text/markdown')
+  }
+  if (allowedExtensions.includes('epub')) {
+    mimeTypes.push('application/epub+zip')
+  }
+  if (allowedExtensions.includes('eml')) {
+    mimeTypes.push('message/rfc822')
+  }
+  if (allowedExtensions.includes('msg')) {
+    mimeTypes.push('application/vnd.ms-outlook')
+  }
+
+  // 添加文件扩展名
+  const extensions = allowedExtensions.map((ext) => `.${ext}`)
+
+  return [...mimeTypes, ...extensions].join(',')
+}
+
 /**
  * 首字母大写
  */

+ 223 - 177
src/views/ai/knowledge/document/create/UploadStep.vue

@@ -1,93 +1,76 @@
 <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)' }}
+  <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-[#dcdfe6] rounded-md 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">
+              <el-icon class="text-[48px] text-[#c0c4cc] mb-10px"><upload-filled /></el-icon>
+              <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-3"
+        >
+          <div
+            v-for="(file, index) in modelData.list"
+            :key="index"
+            class="flex justify-between items-center p-10px border-2 border-[#c0c4cc] rounded-md shadow-sm hover:border-[#409eff] transition-colors duration-300"
+          >
+            <div class="flex items-center">
+              <el-icon class="mr-8px text-[#909399]"><document /></el-icon>
+              <span class="text-[14px] text-[#606266] break-all">{{ file.name }}</span>
+            </div>
+            <el-button type="danger" link @click="removeFile(index)">
+              <el-icon><delete /></el-icon>
+            </el-button>
           </div>
-        </template>
-      </el-upload>
+        </div>
+      </div>
     </el-form-item>
 
     <!-- 添加下一步按钮 -->
     <el-form-item>
-      <div class="flex justify-end">
-        <el-button type="primary" @click="handleNextStep">下一步</el-button>
+      <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 } from 'vue'
-import { UploadFilled } from '@element-plus/icons-vue'
+import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
+import { UploadFilled, Document, Delete } from '@element-plus/icons-vue'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { generateAcceptedFileTypes } from '@/utils'
 
 const props = defineProps({
   modelValue: {
@@ -98,128 +81,191 @@ const props = defineProps({
 
 const emit = defineEmits(['update:modelValue'])
 
-// 表单引用
-const formRef = ref()
+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)
 
-// 获取父组件实例
-const parent = inject('parent', null)
+// 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
+const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
 
-// 表单数据
+/** 表单数据 */
 const modelData = computed({
-  get: () => props.modelValue,
+  get: () => {
+    return props.modelValue
+  },
   set: (val) => emit('update:modelValue', val)
 })
 
-// 知识库列表
-interface KnowledgeBase {
-  id: number
-  name: string
+/** 确保 list 属性存在 */
+const ensureListExists = () => {
+  if (!props.modelValue.list) {
+    emit('update:modelValue', {
+      ...props.modelValue,
+      list: []
+    })
+  }
 }
 
-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 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)
 }
 
-// 文件上传处理
-const handleFileChange = (file) => {
-  modelData.value.file = file.raw
+/**
+ * 文件上传失败处理
+ *
+ * @param error 错误信息
+ * @param file 上传的文件
+ */
+const handleUploadError = (error, file) => {
+  message.error(`文件 ${file.name} 上传失败: ${error}`)
+  // 减少上传中的文件计数
+  uploadingCount.value = Math.max(0, uploadingCount.value - 1)
 }
 
-// 下一步按钮处理
-const handleNextStep = () => {
-  // 获取父组件的goToNextStep方法
-  const parentEl = parent || getCurrentInstance()?.parent
-  if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
-    parentEl.exposed.goToNextStep()
+/**
+ * 文件变更处理
+ *
+ * @param file 变更的文件
+ */
+const handleFileChange = (file) => {
+  if (file.status === 'success' || file.status === 'fail') {
+    uploadingCount.value = Math.max(0, uploadingCount.value - 1)
   }
 }
 
-// 初始化数据
-const initData = async () => {
-  // 获取知识库列表
-  // knowledgeBaseList.value = await KnowledgeBaseApi.getKnowledgeBaseList()
-
-  // 模拟数据
-  knowledgeBaseList.value = [
-    { id: 1, name: '产品知识库' },
-    { id: 2, name: '技术文档库' },
-    { id: 3, name: '客户服务知识库' }
-  ]
+/**
+ * 文件移除处理
+ *
+ * @param file 被移除的文件
+ */
+const handleFileRemove = (file) => {
+  if (file.status === 'uploading') {
+    uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+  }
 }
 
-// 表单校验
-const validate = () => {
-  return new Promise((resolve, reject) => {
-    formRef.value?.validate((valid) => {
-      if (valid) {
-        resolve(true)
-      } else {
-        reject(new Error('请完善表单信息'))
-      }
-    })
+/**
+ * 从列表中移除文件
+ *
+ * @param index 要移除的文件索引
+ */
+const removeFile = (index: number) => {
+  // 从列表中移除文件
+  const newList = [...props.modelValue.list]
+  newList.splice(index, 1)
+  // 更新表单数据
+  emit('update:modelValue', {
+    ...props.modelValue,
+    list: newList
   })
 }
 
-// 对外暴露方法
-defineExpose({
-  validate
-})
+/** 下一步按钮处理 */
+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(() => {
-  initData()
+  ensureListExists()
 })
 </script>
 
-<style lang="scss" scoped>
-.upload-demo {
-  width: 100%;
-}
-</style>
+<style lang="scss" scoped></style>

+ 1 - 6
src/views/ai/knowledge/document/create/index.vue

@@ -90,12 +90,7 @@ const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: '
 // 表单数据
 const formData = ref({
   id: undefined,
-  name: '',
-  knowledgeBaseId: undefined,
-  documentType: undefined,
-  content: '',
-  file: null,
-  segments: [],
+  list: [], // 用于存储上传的文件列表
   status: 0 // 0: 草稿, 1: 处理中, 2: 已完成
 })