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 params: params
}) })
} }
//minerU 获取文档处理状态
export function minerUQuery(params) {
return request({
url: getUrl('/dataset/document/query'),
method: 'get',
params: params,
noLoading: true
})
}
//minerU 获取markdown //minerU 获取markdown
export function minerUMarkDown(params) { export function minerUMarkDown(params) {
return getUrl(`document/mineru/md?documentId=${params.documentId}`) return getUrl(`document/mineru/md?documentId=${params.documentId}`)
@@ -139,9 +148,40 @@ export function updateRuleExtractSplit(data) {
//知识库文件上传-自定义 //知识库文件上传-自定义
export function uploadFileByCustom(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) { 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() { function endLoading() {
//使用Element loading-close 方法 //使用Element loading-close 方法
loading.close() if (loading) {
loading.close()
}
} }
// request interceptor // request interceptor
@@ -32,9 +34,11 @@ service.interceptors.request.use(
if (loading) { if (loading) {
endLoading() endLoading()
} }
console.log(config.noLoading)
//linkage 接口 //linkage 接口
// config.type 可以从api的接口地址定义 可以不触发loading // config.type 可以从api的接口地址定义 可以不触发loading
if (config.type != false) { if (config.noLoading != true) {
startLoading() startLoading()
} }
return config return config
@@ -48,7 +52,7 @@ service.interceptors.response.use(
response => { response => {
const res = response.data const res = response.data
endLoading() endLoading()
if(!res.content){ if (!res.content) {
Message({ Message({
message: res.message, message: res.message,
type: 'error', type: 'error',
@@ -75,7 +79,7 @@ service.interceptors.response.use(
}) })
} else { } else {
Message({ Message({
message: res.content.message || 'Error', message: res.content.message || res.content.resultMessage || 'Error',
type: 'error', type: 'error',
duration: 5 * 1000 duration: 5 * 1000
}) })
@@ -91,8 +95,8 @@ service.interceptors.response.use(
return false return false
} else { } else {
res.content.result = '0' res.content.result = '0'
res.result = String(res.result?res.result:0) res.result = String(res.result ? res.result : 0)
res.code = String(res.code ? res.code: 0 ) res.code = String(res.code ? res.code : 0)
return res return res
} }
} }

View File

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

View File

@@ -13,17 +13,7 @@
</el-form-item> </el-form-item>
<!-- 文件上传--> <!-- 文件上传-->
<el-form-item label="" required prop="file"> <el-form-item label="" required prop="file">
<el-upload <div @click="createFiled" class="upload-demo">
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"
>
<el-empty v-if="!uploadLoading"> <el-empty v-if="!uploadLoading">
<template #description> <template #description>
<p>点击或将文件拖拽到这里上传</p> <p>点击或将文件拖拽到这里上传</p>
@@ -31,7 +21,7 @@
</template> </template>
</el-empty> </el-empty>
<div v-else v-loading="uploadLoading" :element-loading-text="'文件上传中...'" style="height: 100%"></div> <div v-else v-loading="uploadLoading" :element-loading-text="'文件上传中...'" style="height: 100%"></div>
</el-upload> </div>
</el-form-item> </el-form-item>
<!-- 本地文件 --> <!-- 本地文件 -->
@@ -80,8 +70,7 @@ export default {
name: 'preprocessing', name: 'preprocessing',
data() { data() {
return { return {
uploadFileByCustom, filed: null,
uploadFileByTemplate,
uploadLoading: false, uploadLoading: false,
headers: {}, headers: {},
fieldList: [], fieldList: [],
@@ -92,7 +81,7 @@ export default {
datasetId: this.$route.query.datasetId datasetId: this.$route.query.datasetId
}, },
previewDialogVisible: false, // 添加对话框显示控制变量 previewDialogVisible: false, // 添加对话框显示控制变量
documentId: null documentId: '1361400636462174208'
} }
}, },
props: {}, props: {},
@@ -104,6 +93,41 @@ export default {
this.$emit('getFileType', this.form.radio) this.$emit('getFileType', this.form.radio)
}, },
downloadTemplate() {}, 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) { handleUploadSuccess(response, file) {
this.fieldList = [file] this.fieldList = [file]
this.uploadLoading = false this.uploadLoading = false

View File

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

View File

@@ -86,29 +86,50 @@
</div> </div>
</el-card> </el-card>
<el-drawer :visible.sync="drawer" size="50%" :title="descriptions.knowledgeName"> <el-drawer :visible.sync="drawer" size="80%" :title="descriptions.knowledgeName">
<div class="flex mh20"> <div class="flex ph20 align-items-b" style="width: 100%">
<div> <div>
<el-descriptions class="margin-top" :column="1" border> <el-descriptions class="margin-top" :column="1" border>
<el-descriptions-item label="分段模式:"> </el-descriptions-item> <el-descriptions-item label="分段模式:">
<el-descriptions-item label="是否使用预处理:"> </el-descriptions-item> {{ segmentedMode | filterSegmentedMode }}
<el-descriptions-item label="是否使用ocr协助处理"> </el-descriptions-item> </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-item label="知识题词规则:"> </el-descriptions-item> <el-descriptions-item label="知识题词规则:"> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </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> </div>
</el-drawer> </el-drawer>
</div> </div>
</template> </template>
<script> <script>
import { getDatasetById, datasetUpdate, datasetsExPages } from '@/api/generatedApi/index' import { getDatasetById, datasetUpdate, datasetsExPages, datasetQuerySegments, datasetQueryDelete } from '@/api/generatedApi/index'
import { documentSourceOptions } from '@/assets/js/utils/utilOptions' import { documentSourceOptions, segmentedModeOptionsMap } from '@/assets/js/utils/utilOptions'
export default { export default {
name: 'index', name: 'index',
data() { data() {
return { return {
activeName: -1,
drawer: false, drawer: false,
editKnowledge: false, editKnowledge: false,
form: { form: {
@@ -239,8 +260,34 @@ export default {
}, },
// 查看文档详情 // 查看文档详情
viewDocumentDetail(row) { viewDocumentDetail(row) {
this.descriptions = row datasetQuerySegments({ documentId: row.id }).then(res => {
this.drawer = true 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: { filters: {
@@ -253,6 +300,10 @@ export default {
default: default:
return '' return ''
} }
},
filterUseMineru(val) {
let item = segmentedModeOptionsMap.find(item => item.value === String(val))
return item ? item.label : '否'
} }
}, },
created() {}, created() {},
@@ -323,7 +374,9 @@ export default {
type: 'danger', type: 'danger',
size: 'mini' size: 'mini'
}, },
on: {} on: {
click: () => this.deleteKnowledge(params.row)
}
}, },
'删除' '删除'
), ),
@@ -349,4 +402,19 @@ export default {
} }
} }
</script> </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>