Commit 7cd6a5d9 by YunaiV

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

parent b7d7b11d
...@@ -117,6 +117,64 @@ export function toAnyString() { ...@@ -117,6 +117,64 @@ export function toAnyString() {
} }
/** /**
* 根据支持的文件类型生成 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(',')
}
/**
* 首字母大写 * 首字母大写
*/ */
export function firstUpperCase(str: string) { export function firstUpperCase(str: string) {
......
<template> <template>
<el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> <el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
<el-form-item label="文档名称" prop="name" class="mb-20px"> <el-form-item class="mb-20px">
<el-input v-model="modelData.name" clearable placeholder="请输入文档名称" /> <div class="w-full">
</el-form-item> <div class="w-full border-2 border-[#dcdfe6] rounded-md text-center hover:border-[#409eff]">
<el-form-item label="知识库" prop="knowledgeBaseId" class="mb-20px"> <el-upload
<el-select ref="uploadRef"
class="!w-full" class="upload-demo"
v-model="modelData.knowledgeBaseId" drag
clearable :action="uploadUrl"
placeholder="请选择知识库" :auto-upload="true"
> :on-success="handleUploadSuccess"
<el-option :on-error="handleUploadError"
v-for="base in knowledgeBaseList" :on-change="handleFileChange"
:key="base.id" :on-remove="handleFileRemove"
:label="base.name" :before-upload="beforeUpload"
:value="base.id" :http-request="httpRequest"
/> :file-list="fileList"
</el-select> :multiple="true"
</el-form-item> :show-file-list="false"
<el-form-item label="文档类型" prop="documentType" class="mb-20px"> :accept="acceptedFileTypes"
<el-select >
class="!w-full" <div class="flex flex-col items-center justify-center py-20px">
v-model="modelData.documentType" <el-icon class="text-[48px] text-[#c0c4cc] mb-10px"><upload-filled /></el-icon>
clearable <div class="el-upload__text text-[16px] text-[#606266]"
placeholder="请选择文档类型" >拖拽文件至此,或者
> <em class="text-[#409eff] not-italic cursor-pointer">选择文件</em></div
<el-option label="PDF文档" value="pdf" /> >
<el-option label="Word文档" value="word" /> <div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
<el-option label="文本文件" value="text" /> 已支持 {{ supportedFileTypes.join('、') }},每个文件不超过 {{ maxFileSize }} MB。
<el-option label="网页链接" value="url" /> </div>
</el-select> </div>
</el-form-item> </el-upload>
<el-form-item </div>
label="文档内容"
prop="content" <div
class="mb-20px" v-if="modelData.list && modelData.list.length > 0"
v-if="modelData.documentType === 'text'" class="mt-15px grid grid-cols-1 gap-3"
> >
<el-input <div
v-model="modelData.content" v-for="(file, index) in modelData.list"
type="textarea" :key="index"
:rows="6" class="flex justify-between items-center p-10px border-2 border-[#c0c4cc] rounded-md shadow-sm hover:border-[#409eff] transition-colors duration-300"
placeholder="请输入文档内容" >
/> <div class="flex items-center">
</el-form-item> <el-icon class="mr-8px text-[#909399]"><document /></el-icon>
<el-form-item <span class="text-[14px] text-[#606266] break-all">{{ file.name }}</span>
label="网页链接" </div>
prop="url" <el-button type="danger" link @click="removeFile(index)">
class="mb-20px" <el-icon><delete /></el-icon>
v-if="modelData.documentType === 'url'" </el-button>
>
<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> </div>
</template> </div>
</el-upload> </div>
</el-form-item> </el-form-item>
<!-- 添加下一步按钮 --> <!-- 添加下一步按钮 -->
<el-form-item> <el-form-item>
<div class="flex justify-end"> <div class="flex justify-end w-full">
<el-button type="primary" @click="handleNextStep">下一步</el-button> <el-button type="primary" @click="handleNextStep" :disabled="!isAllUploaded">
下一步
</el-button>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { PropType } from 'vue' import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
import { UploadFilled } from '@element-plus/icons-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({ const props = defineProps({
modelValue: { modelValue: {
...@@ -98,128 +81,191 @@ const props = defineProps({ ...@@ -98,128 +81,191 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) 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)
// 获取父组件实例 // 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
const parent = inject('parent', null) const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
// 表单数据 /** 表单数据 */
const modelData = computed({ const modelData = computed({
get: () => props.modelValue, get: () => {
return props.modelValue
},
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val)
}) })
// 知识库列表 /** 确保 list 属性存在 */
interface KnowledgeBase { const ensureListExists = () => {
id: number if (!props.modelValue.list) {
name: string emit('update:modelValue', {
...props.modelValue,
list: []
})
}
} }
const knowledgeBaseList = ref<KnowledgeBase[]>([]) /** 是否所有文件都已上传完成 */
const isAllUploaded = computed(() => {
// 表单校验规则 return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
const rules = { })
name: [{ required: true, message: '请输入文档名称', trigger: 'blur' }],
knowledgeBaseId: [{ required: true, message: '请选择知识库', trigger: 'change' }], /**
documentType: [{ required: true, message: '请选择文档类型', trigger: 'change' }], * 上传前检查文件类型和大小
content: [ *
{ * @param file 待上传的文件
required: true, * @returns 是否允许上传
message: '请输入文档内容', */
trigger: 'blur', const beforeUpload = (file) => {
validator: (rule, value, callback) => { // 1.1 检查文件扩展名
if (modelData.value.documentType === 'text' && !value) { const fileName = file.name.toLowerCase()
callback(new Error('请输入文档内容')) const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
} else { if (!allowedExtensions.includes(fileExtension)) {
callback() message.error('不支持的文件类型!')
} return false
} }
} // 1.2 检查文件大小
], if (!(file.size / 1024 / 1024 < maxFileSize)) {
url: [ message.error(`文件大小不能超过 ${maxFileSize} MB!`)
{ return false
required: true, }
message: '请输入网页链接',
trigger: 'blur', // 2. 增加上传中的文件计数
validator: (rule, value, callback) => { uploadingCount.value++
if (modelData.value.documentType === 'url' && !value) { return true
callback(new Error('请输入网页链接')) }
} else {
callback() /**
} * 文件上传成功处理
} *
} * @param response 上传响应
], * @param file 上传的文件
file: [ */
{ const handleUploadSuccess = (response, file) => {
required: true, if (response && response.data) {
message: '请上传文件', // 添加到文件列表
trigger: 'change', ensureListExists()
validator: (rule, value, callback) => { emit('update:modelValue', {
if (['pdf', 'word'].includes(modelData.value.documentType) && !modelData.value.file) { ...props.modelValue,
callback(new Error('请上传文件')) list: [
} else { ...props.modelValue.list,
callback() {
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 * @param file 变更的文件
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') { */
parentEl.exposed.goToNextStep() 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() * @param file 被移除的文件
*/
// 模拟数据 const handleFileRemove = (file) => {
knowledgeBaseList.value = [ if (file.status === 'uploading') {
{ id: 1, name: '产品知识库' }, uploadingCount.value = Math.max(0, uploadingCount.value - 1)
{ id: 2, name: '技术文档库' }, }
{ id: 3, name: '客户服务知识库' }
]
} }
// 表单校验 /**
const validate = () => { * 从列表中移除文件
return new Promise((resolve, reject) => { *
formRef.value?.validate((valid) => { * @param index 要移除的文件索引
if (valid) { */
resolve(true) const removeFile = (index: number) => {
} else { // 从列表中移除文件
reject(new Error('请完善表单信息')) const newList = [...props.modelValue.list]
} newList.splice(index, 1)
}) // 更新表单数据
emit('update:modelValue', {
...props.modelValue,
list: newList
}) })
} }
// 对外暴露方法 /** 下一步按钮处理 */
defineExpose({ const handleNextStep = () => {
validate // 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(() => { onMounted(() => {
initData() ensureListExists()
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped></style>
.upload-demo {
width: 100%;
}
</style>
...@@ -90,12 +90,7 @@ const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: ' ...@@ -90,12 +90,7 @@ const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: '
// 表单数据 // 表单数据
const formData = ref({ const formData = ref({
id: undefined, id: undefined,
name: '', list: [], // 用于存储上传的文件列表
knowledgeBaseId: undefined,
documentType: undefined,
content: '',
file: null,
segments: [],
status: 0 // 0: 草稿, 1: 处理中, 2: 已完成 status: 0 // 0: 草稿, 1: 处理中, 2: 已完成
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment