feat(knowledge): 实现知识库文件上传和预处理功能

- 新增文件上传接口和相关组件
- 实现文件上传、预处理和保存功能
- 添加预处理结果预览功能
-优化知识库详情展示,增加文件内容和关键词显示
- 实现知识库文件删除功能
- TODO 拆分的文档展示  还需要调整  可能会把tags 放在标题上  需要修改样式
This commit is contained in:
陈昱达
2025-04-14 21:18:56 +08:00
parent 7f123159a3
commit bcff977d38
7 changed files with 263 additions and 57 deletions

View File

@@ -56,6 +56,15 @@ export function minerUBbox(params) {
params: params
})
}
//minerU 获取文档处理状态
export function minerUQuery(params) {
return request({
url: getUrl('/dataset/document/query'),
method: 'get',
params: params,
noLoading: true
})
}
//minerU 获取markdown
export function minerUMarkDown(params) {
return getUrl(`document/mineru/md?documentId=${params.documentId}`)
@@ -139,9 +148,40 @@ export function updateRuleExtractSplit(data) {
//知识库文件上传-自定义
export function uploadFileByCustom(data) {
return getUrl(`/datasetDocumentEx/uploadFileByCustom`, 'jifen')
return request({
url: getUrl(`/datasetDocumentEx/uploadFileByCustom`),
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
//知识库文件上传-通用
export function uploadFileByTemplate(data) {
return getUrl(`/datasetDocumentEx/uploadFileByTemplate`, 'jifen')
return request({
url: getUrl(`/datasetDocumentEx/uploadFileByTemplate`),
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 知识库文件详情
export function datasetQuerySegments(data) {
return request({
url: getUrl(`/datasetDocumentEx/querySegments`, 'jifen'),
method: 'get',
params: data
})
}
// 知识库文件删除
export function datasetQueryDelete(data) {
return request({
url: getUrl(`/datasetDocumentEx/delete?documentId=${data.id}`, 'jifen'),
method: 'delete',
data
})
}

View File

@@ -18,7 +18,9 @@ function startLoading() {
}
function endLoading() {
//使用Element loading-close 方法
loading.close()
if (loading) {
loading.close()
}
}
// request interceptor
@@ -32,9 +34,11 @@ service.interceptors.request.use(
if (loading) {
endLoading()
}
console.log(config.noLoading)
//linkage 接口
// config.type 可以从api的接口地址定义 可以不触发loading
if (config.type != false) {
if (config.noLoading != true) {
startLoading()
}
return config
@@ -48,7 +52,7 @@ service.interceptors.response.use(
response => {
const res = response.data
endLoading()
if(!res.content){
if (!res.content) {
Message({
message: res.message,
type: 'error',
@@ -75,7 +79,7 @@ service.interceptors.response.use(
})
} else {
Message({
message: res.content.message || 'Error',
message: res.content.message || res.content.resultMessage || 'Error',
type: 'error',
duration: 5 * 1000
})
@@ -91,8 +95,8 @@ service.interceptors.response.use(
return false
} else {
res.content.result = '0'
res.result = String(res.result?res.result:0)
res.code = String(res.code ? res.code: 0 )
res.result = String(res.result ? res.result : 0)
res.code = String(res.code ? res.code : 0)
return res
}
}

View File

@@ -8,3 +8,25 @@ export const documentSourceOptions = [
value: '1'
}
]
export const segmentedModeOptions = [
{
label: '传统分段模式',
value: '0'
},
{
label: 'Q&A分段模式',
value: '1'
}
]
export const segmentedModeOptionsMap = [
{
label: '是',
value: '0'
},
{
label: '否',
value: '1'
}
]

View File

@@ -1,8 +1,7 @@
<template>
<div style="height: 100%;">
<div class="mv10 mh20 text-right" v-if="isEdit">
<el-button type="primary" size="medium" @click="saveMarkDown">保存并处理</el-button>
<el-button size="medium">取消</el-button>
<el-button type="primary" size="medium" @click="saveMarkDown" :disabled="finishedMiner">保存并处理</el-button>
</div>
<div :class="!isEdit ? 'mt10 flex' : 'flex'" style="height:calc(100% - 35px);flex:1">
<iframe
@@ -11,7 +10,7 @@
:src="`${iframeSrc}/pdfjs-dist/web/viewer.html?file=${encodeURIComponent(prdUrl)}`"
class="miner-u el-card is-always-shadow ml20"
></iframe>
<div style="flex:1;" class="mh20 miner-u-md">
<div style="flex:1;" class="mh20 miner-u-md" v-loading="finishedMiner" element-loading-text="正在识别中...">
<el-tabs type="border-card" style="height: 100%;overflow: hidden" @tab-click="changeTab">
<el-tab-pane label="预览" style="overflow:scroll;">
<div v-html="markdownHtml" class="view-body" id="viewBody" ref="viewBody"></div>
@@ -27,7 +26,7 @@
</div>
</template>
<script>
import { getPdfUrl, minerUBbox, minerUMarkDown, minerUMarkDownUpdate } from '@/api/generatedApi/index'
import { getPdfUrl, minerUBbox, minerUMarkDown, minerUMarkDownUpdate, minerUQuery } from '@/api/generatedApi/index'
import { DEFAULT_COLOR_SECTION, PDF_COLOR_PICKER } from './pdf-color'
import MarkdownIt from 'markdown-it'
import markdownItKatex from 'markdown-it-katex'
@@ -40,6 +39,7 @@ export default {
data() {
return {
tableIdCounter: 0,
finishedMiner: false,
prdUrl: ``,
iframeSrc: window.location.origin,
bboxList: [],
@@ -103,7 +103,18 @@ export default {
default: true
}
},
watch: {},
watch: {
documentId: {
handler(newVal, oldVal) {
if (newVal) {
this.getMinerUStatus()
this.prdUrl = getPdfUrl({ documentId: newVal })
}
},
immediate: true
}
},
components: {},
filters: {},
methods: {
@@ -171,9 +182,10 @@ export default {
minerUMarkDownUpdate({
documentId: this.documentId,
newMd: pre
}).then(res => {
this.$emit('saveMarkDown', true)
})
},
// 给文件增加色块
formatJson(data) {
return data.map(item => {
@@ -245,13 +257,36 @@ export default {
'*'
)
}
},
// 获取识别状态
getMinerUStatus() {
this.finishedMiner = true
minerUQuery({ id: this.documentId }).then(res => {
console.log(res)
let mineruStatus = res.content.content.mineruStatus
switch (mineruStatus) {
case 0:
case '0':
setTimeout(() => {
this.getMinerUStatus()
}, 1000)
break
case 1:
case '1':
this.finishedMiner = false
this.getPDFDetailBbox()
this.getPDFDetailMarkDown()
break
default:
this.finishedMiner = false
break
}
})
}
},
created() {
this.getPDFDetailBbox()
this.getPDFDetailMarkDown()
this.prdUrl = getPdfUrl({ documentId: this.documentId })
},
created() {},
mounted() {
// 监听 iframe 的 postMessage 事件
window.addEventListener('message', event => {

View File

@@ -13,17 +13,7 @@
</el-form-item>
<!-- 文件上传-->
<el-form-item label="" required prop="file">
<el-upload
drag
:action="form.radio === '1' ? uploadFileByCustom() : uploadFileByTemplate()"
:headers="headers"
class="upload-demo"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:show-file-list="false"
:file-list="fieldList"
:data="form"
>
<div @click="createFiled" class="upload-demo">
<el-empty v-if="!uploadLoading">
<template #description>
<p>点击或将文件拖拽到这里上传</p>
@@ -31,7 +21,7 @@
</template>
</el-empty>
<div v-else v-loading="uploadLoading" :element-loading-text="'文件上传中...'" style="height: 100%"></div>
</el-upload>
</div>
</el-form-item>
<!-- 本地文件 -->
@@ -80,8 +70,7 @@ export default {
name: 'preprocessing',
data() {
return {
uploadFileByCustom,
uploadFileByTemplate,
filed: null,
uploadLoading: false,
headers: {},
fieldList: [],
@@ -92,7 +81,7 @@ export default {
datasetId: this.$route.query.datasetId
},
previewDialogVisible: false, // 添加对话框显示控制变量
documentId: null
documentId: '1361400636462174208'
}
},
props: {},
@@ -104,6 +93,41 @@ export default {
this.$emit('getFileType', this.form.radio)
},
downloadTemplate() {},
// 生成filed
createFiled() {
let input = document.createElement('input')
input.type = 'file'
input.onchange = e => {
// this.$emit('getFile', e.target.files[0])
this.filed = e.target.files[0]
}
input.click()
},
uploadFiled() {
let formData = new FormData()
formData.append('file', this.filed)
formData.append('datasetId', this.form.datasetId)
let api = this.form.radio === '1' ? uploadFileByCustom : uploadFileByTemplate
if (this.form.radio === '1') {
formData.append('beMinerU', this.form.beMinerU)
formData.append('beOcr', this.form.beOcr)
}
api(formData).then(response => {
this.documentId = response.content.content
// 向上导入documentId
this.$emit('getDocumentId', this.documentId)
// 文件内容
this.$emit('getUploadDetail', response.content.content)
// 表单内容
this.$emit('getForm', this.form, this.$refs.processForm)
if (this.form.beMinerU) {
this.$emit('beMinerU', true)
}
})
},
handleUploadSuccess(response, file) {
this.fieldList = [file]
this.uploadLoading = false

View File

@@ -12,9 +12,9 @@
</el-steps>
<div class="components">
<step-preprocessing v-if="active === 0" @getForm="getForm" @getDocumentId="getDocumentId" @beMinerU="beMinerU"></step-preprocessing>
<step-split-config ref="splitConfig" v-if="active === 1"></step-split-config>
<step-words v-if="active === 2"></step-words>
<step-preprocessing v-show="active === 0" @getForm="getForm" @getDocumentId="getDocumentId" ref="stepPreProcessing"></step-preprocessing>
<step-split-config ref="splitConfig" v-show="active === 1"></step-split-config>
<step-words v-show="active === 2"></step-words>
</div>
</div>
@@ -26,14 +26,14 @@
<el-button size="medium" @click="active--" v-if="active >= 1">上一步</el-button>
<!-- <el-button type="primary" size="medium" @click="active++" v-if="active < 2">下一步</el-button>-->
<el-button type="primary" size="medium" @click="nextStep" v-if="active < 2">下一步</el-button>
<el-button type="primary" size="medium">取消</el-button>
<el-button type="primary" size="medium" @click="$router.history.go(-1)">取消</el-button>
</div>
</el-card>
<!-- 添加预处理结果预览对话框 -->
<el-drawer :visible.sync="visible" size="80%" title="预处理结果预览">
<div style="height:calc(100% - 55px);">
<r-miner-u :documentId="documentId"></r-miner-u>
<r-miner-u :documentId="documentId" @saveMarkDown="saveMarkDown"></r-miner-u>
</div>
</el-drawer>
</div>
@@ -47,9 +47,9 @@ export default {
name: 'create',
data() {
return {
visible: true,
visible: false,
active: 0,
documentId: '1361352503568994304'
documentId: '1361407835582337024'
}
},
props: {},
@@ -61,17 +61,30 @@ export default {
},
filters: {},
methods: {
beMinerU() {
this.visible = true
saveMarkDown() {
this.visible = false
this.active++
},
beMinerU() {
setTimeout(() => {
this.visible = true
})
},
getForm(form) {
if (form.beMinerU) {
this.beMinerU()
} else {
this.saveMarkDown()
}
},
getForm() {},
fetchApi() {},
getDocumentId(id) {
this.documentId = id
},
nextStep() {
if (this.active === 0) {
this.active++
// this.active++
this.$refs.stepPreProcessing.uploadFiled()
} else if (this.active === 1) {
this.$refs.splitConfig.nextStep()
}

View File

@@ -86,29 +86,50 @@
</div>
</el-card>
<el-drawer :visible.sync="drawer" size="50%" :title="descriptions.knowledgeName">
<div class="flex mh20">
<el-drawer :visible.sync="drawer" size="80%" :title="descriptions.knowledgeName">
<div class="flex ph20 align-items-b" style="width: 100%">
<div>
<el-descriptions class="margin-top" :column="1" border>
<el-descriptions-item label="分段模式:"> </el-descriptions-item>
<el-descriptions-item label="是否使用预处理:"> </el-descriptions-item>
<el-descriptions-item label="是否使用ocr协助处理"> </el-descriptions-item>
<el-descriptions-item label="分段模式:">
{{ segmentedMode | filterSegmentedMode }}
</el-descriptions-item>
<el-descriptions-item label="是否使用预处理:">
{{ descriptions.useMineru | filterUseMineru }}
</el-descriptions-item>
<el-descriptions-item label="是否使用ocr协助处理">
{{ descriptions.mineruUseOcr | filterUseMineru }}
</el-descriptions-item>
<el-descriptions-item label="知识拆分规则:"> </el-descriptions-item>
<el-descriptions-item label="知识题词规则:"> </el-descriptions-item>
</el-descriptions>
</div>
<div></div>
<div style="width:80%" class="ml20">
<el-collapse v-model="activeName" accordion>
<el-collapse-item v-for="(item, index) in descriptions.data" :name="index" style="width: 100%" :key="index">
<div slot="title" class="flex" style="width: 100%">
<div class="ellipsis-title" style="flex:1">{{ item.content }}</div>
</div>
<div>
{{ item.content }}
</div>
<div class="mt20" style="flex:1">
<el-tag v-for="(tags, index) in item.keywords" :label="tags" size="mini" type="info" class="mr10 " :key="index">{{ tags }}</el-tag>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</el-drawer>
</div>
</template>
<script>
import { getDatasetById, datasetUpdate, datasetsExPages } from '@/api/generatedApi/index'
import { documentSourceOptions } from '@/assets/js/utils/utilOptions'
import { getDatasetById, datasetUpdate, datasetsExPages, datasetQuerySegments, datasetQueryDelete } from '@/api/generatedApi/index'
import { documentSourceOptions, segmentedModeOptionsMap } from '@/assets/js/utils/utilOptions'
export default {
name: 'index',
data() {
return {
activeName: -1,
drawer: false,
editKnowledge: false,
form: {
@@ -239,8 +260,34 @@ export default {
},
// 查看文档详情
viewDocumentDetail(row) {
this.descriptions = row
this.drawer = true
datasetQuerySegments({ documentId: row.id }).then(res => {
if (res) {
this.descriptions = {
...row,
...res.content.content
}
this.drawer = true
}
})
},
// 删除文件
deleteKnowledge(row) {
this.$messageBox(
() => {
datasetQueryDelete({ id: row.id }).then(res => {
if (res) {
this.$message({
type: 'success',
message: '删除成功!'
})
this.page = 1
this.getList()
}
})
},
'是否确认删除当前知识文件?',
'warning'
)
}
},
filters: {
@@ -253,6 +300,10 @@ export default {
default:
return ''
}
},
filterUseMineru(val) {
let item = segmentedModeOptionsMap.find(item => item.value === String(val))
return item ? item.label : '否'
}
},
created() {},
@@ -323,7 +374,9 @@ export default {
type: 'danger',
size: 'mini'
},
on: {}
on: {
click: () => this.deleteKnowledge(params.row)
}
},
'删除'
),
@@ -349,4 +402,19 @@ export default {
}
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.ellipsis-title {
// 最多展示一行 省略号展示
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
word-break: break-all;
white-space: nowrap;
//width: 50%;
&:hover {
color: #409eff;
}
}
</style>