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