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

This commit is contained in:
YunaiV 2025-02-28 19:56:18 +08:00
parent 94091598a3
commit b7d7b11d31
7 changed files with 929 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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