mirror of
https://gitee.com/myxzgzs/boyue-ui-admin-vue3
synced 2025-08-08 16:32:43 +08:00
【功能新增】AI:知识库文档上传:10% 搭建整体页面结构
This commit is contained in:
parent
94091598a3
commit
b7d7b11d31
@ -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
src/views/ai/knowledge/document/create/ProcessStep.vue
Normal file
235
src/views/ai/knowledge/document/create/ProcessStep.vue
Normal 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>
|
234
src/views/ai/knowledge/document/create/SplitStep.vue
Normal file
234
src/views/ai/knowledge/document/create/SplitStep.vue
Normal 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>
|
225
src/views/ai/knowledge/document/create/UploadStep.vue
Normal file
225
src/views/ai/knowledge/document/create/UploadStep.vue
Normal 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>
|
214
src/views/ai/knowledge/document/create/index.vue
Normal file
214
src/views/ai/knowledge/document/create/index.vue
Normal 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>
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user