refactor(knowledge): 重构知识库详情页面

- 将 DocumentDrawer 组件抽离为独立模块
- 新增 QAModel 和 TextModel 组件用于展示不同类型的分段内容
- 优化知识库详情页面的结构和样式- 提升代码的可维护性和可复用性
This commit is contained in:
du.meimei
2025-04-17 17:41:17 +08:00
parent e143bc902e
commit 783c06f9f5
7 changed files with 577 additions and 149 deletions

View File

@@ -0,0 +1,286 @@
<!-- src/views/knowledge/detail/components/DocumentDrawer.vue -->
<template>
<el-drawer :visible.sync="visible" size="80%" :title="descriptions.knowledgeName" @close="$emit('update:visible', false)">
<!-- 原drawer内容整体拷贝至此 -->
<div class="drawer-content">
<div class="file-header">
<div class="file-icon">
<i class="el-icon-document"></i>
</div>
<div class="file-info">
<h3>{{ descriptions.fileName || descriptions.knowledgeName }}</h3>
<el-tag type="primary" class="mr10">{{ getDocumentSourceLabel(descriptions.documentSource) }}</el-tag>
<el-tag type="info">{{ descriptions.createdDate }}</el-tag>
</div>
</div>
<el-divider></el-divider>
<div class="segment-info">
<div class="segment-header">
<h4>分段规则</h4>
</div>
<div class="rule-card">
<div class="rule-item">
<h5>是否使用预处理:</h5>
<span>{{ descriptions.usePreProcess | filterUseMineru }}</span>
</div>
<div class="rule-item">
<h5>是否使用OCR协助处理:</h5>
<span>{{ descriptions.useOcr | filterUseMineru }}</span>
</div>
<div class="rule-item">
<h5>知识拆分规则:</h5>
<span v-if="!documentDetail.splitRules">
-
</span>
<div v-else class="flex">
<div v-for="item in documentDetail.splitRules" :key="item.id" class="rule-detail-item">
<p>
<span class="rule-label">样式:</span> <span class="rule-value">{{ item.titleLevel }}</span>
</p>
<p>
<span class="rule-label">关键词:</span> <span class="rule-value">{{ item.ruleRegex }}</span>
</p>
<p>
<span class="rule-label">备注:</span> <span class="rule-value">{{ item.description }}</span>
</p>
</div>
</div>
</div>
<div class="rule-item">
<h5>知识题词规则:</h5>
<span v-if="!documentDetail.extractRules">
-
</span>
<div v-else class="flex">
<div v-for="item in documentDetail.extractRules" :key="item.id" class="rule-detail-item">
<p>
<span class="rule-label">属性:</span> <span class="rule-value">{{ item.attribute }}</span>
</p>
<p>
<span class="rule-label">属性描述:</span><span class="rule-value">{{ item.attributeDesc }}</span>
</p>
<p>
<span class="rule-label">关键词:</span> <span class="rule-value">{{ item.keyword }}</span>
</p>
<p>
<span class="rule-label">关键词示例:</span> <span class="rule-value">{{ item.example }}</span>
</p>
<p>
<span class="rule-label">提示词:</span> <span class="rule-value">{{ item.prompt }}</span>
</p>
</div>
</div>
</div>
</div>
</div>
<el-divider></el-divider>
<div class="segment-content-container">
<div class="segment-header mb15">
<h4>分段内容</h4>
<div class="segment-summary">
<span>{{ descriptions.data ? descriptions.data.length : 0 }}个分段</span>
</div>
</div>
<text-model
v-if="descriptions.doc_form === 'text_model'"
:descriptions="descriptions"
:documentDetail="documentDetail"
:activeSegment="activeSegment"
/>
<q-a-model
v-else-if="descriptions.doc_form === 'qa_model'"
:descriptions="descriptions"
:documentDetail="documentDetail"
:activeSegment="activeSegment"
/>
</div>
</div>
</el-drawer>
</template>
<script>
import TextModel from './TextModel.vue'
import QAModel from './QAModel.vue'
import { segmentedModeOptionsMap } from '@/assets/js/utils/utilOptions'
export default {
components: { TextModel, QAModel },
props: {
visible: Boolean,
descriptions: {
type: Object,
default: () => ({ data: [] })
},
documentDetail: {
type: Object,
default: () => ({})
},
activeSegment: {
type: Number,
default: 0
}
},
data() {
return {
localActiveSegment: this.activeSegment
}
},
watch: {
activeSegment(newVal) {
this.localActiveSegment = newVal
},
// 深度监听descriptions变化
descriptions: {
handler(newVal) {
// 触发重新渲染的逻辑
this.$nextTick(() => {
this.localActiveSegment = 0 // 重置激活分段
this.$forceUpdate() // 强制刷新(仅在必要情况使用)
})
},
deep: true, // 必须开启深度监听
immediate: true // 初始化立即执行
}
},
filters: {
filterSegmentedMode(val) {
switch (val) {
case 0:
return '传统分段模式'
case 1:
return ' Q&A分段模式'
default:
return ''
}
},
filterUseMineru(val) {
let item = segmentedModeOptionsMap.find(item => item.value === String(val))
return item ? item.label : '否'
}
},
methods: {
getDocumentSourceLabel(sourceValue) {
// 保持原方法实现
}
}
}
</script>
<style scoped lang="scss">
.drawer-content {
padding: 20px;
display: flex;
flex-direction: column;
}
.file-header {
display: flex;
align-items: center;
margin-bottom: 20px;
.file-icon {
font-size: 36px;
margin-right: 15px;
color: #409eff;
}
.file-info {
h3 {
margin: 0 0 10px 0;
font-size: 18px;
font-weight: bold;
}
}
}
.segment-info {
margin-top: 20px;
padding: 15px;
background: #f5f7fa;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.segment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
h4 {
margin: 0;
font-size: 18px;
color: #333;
}
}
.rule-card {
//background: #fff;
//border-radius: 4px;
//padding: 15px;
//box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
.rule-item {
margin-bottom: 12px;
h5 {
display: inline-block;
margin: 0 8px 0 0;
font-weight: bold;
color: #666;
}
&:last-child {
margin-bottom: 0;
}
}
.rule-detail-item {
margin-top: 8px;
padding: 10px;
background: #f9f9f9;
border-radius: 8px;
border: 1px solid #e0e0e0;
margin-right: 10px;
p {
margin: 5px 0;
}
.rule-label {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.rule-value {
font-size: 14px;
color: #606266;
font-weight: 500;
margin-left: 15px;
}
}
}
}
.segment-content-container {
margin-top: 20px;
}
.segment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
h4 {
margin: 0;
font-size: 16px;
}
.segment-summary {
color: #606266;
}
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<div class="segment-split-view">
<div class="segment-list">
<div
v-for="(segment, index) in descriptions.data"
:key="index"
class="segment-list-item"
:class="{ active: activeSegment === index }"
@click="activeSegment = index"
>
<div>
<span class="segment-number">分段 - {{ index + 1 }}</span> · <span v-if="segment.word_count > 0">{{ segment.word_count }}个字符</span>
</div>
<div>
<p>Q {{ segment.content }}</p>
<p>A {{ segment.answer }}</p>
</div>
<!-- <div class="segment-keywords flex" v-if="segment.keywords && segment.keywords.length">-->
<!-- <p v-for="(item, index) in segment.keywords" :key="index" class="mr10">#{{ item }}</p>-->
<!-- </div>-->
<!-- <span class="segment-chars">{{ segment.characters || 0 }} characters</span>-->
</div>
</div>
<div class="segment-detail" v-if="descriptions.data && descriptions.data.length > 0">
<div class="segment-content" v-if="activeSegment !== null">
<div>
<div>
<p>QUESTION</p>
<p>{{ descriptions.data[activeSegment].content }}</p>
</div>
<div>
<p>ANSWER</p>
<p>{{ descriptions.data[activeSegment].answer }}</p>
</div>
</div>
<div class="flex align-items-c mt20" v-if="descriptions.data[activeSegment].keywords && descriptions.data[activeSegment].keywords.length">
关键词
<el-tag v-for="(item, index) in descriptions.data[activeSegment].keywords" :key="index" class="mr10" size="mini" type="primary">
{{ item }}
</el-tag>
</div>
</div>
<div v-if="activeSegment !== null"></div>
<div class="segment-empty" v-else>
<el-empty description="请选择左侧分段查看详情"></el-empty>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'QAModel',
props: {
visible: Boolean,
descriptions: {
type: Object,
default: () => ({ data: [{}] })
},
documentDetail: {
type: Object,
default: () => ({})
},
activeSegment: {
type: Number,
default: 0
}
}
}
</script>
<style scoped lang="scss">
.segment-split-view {
display: flex;
}
.segment-list {
//flex: 1;
width: 45%;
margin-right: 10px;
}
.segment-list-item {
cursor: pointer;
margin-bottom: 10px;
padding: 15px 0 10px 15px;
border-radius: 15px;
transition: background-color 0.3s;
font-size: 14px;
&:hover {
background: #f3f5f7;
}
&.active {
background: #f3f5f7;
}
p {
margin: 10px 0;
}
.segment-number {
color: #0a84ff;
}
.segment-keywords {
color: #606266;
}
}
.segment-detail {
width: 55%;
}
.segment-content {
padding: 20px;
background: #f9f9f9;
border-radius: 15px;
//white-space: pre-wrap;
line-height: 35px;
color: #666;
}
.segment-empty {
padding: 10px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<div class="segment-split-view">
<div class="segment-list">
<div
v-for="(segment, index) in descriptions.data"
:key="index"
class="segment-list-item"
:class="{ active: activeSegment === index }"
@click="activeSegment = index"
>
<div>
<span class="segment-number">分段 - {{ index + 1 }}</span> · <span v-if="segment.word_count > 0">{{ segment.word_count }}个字符</span>
</div>
<p>{{ segment.content.slice(0, 20) + '.....' }}</p>
<div class="segment-keywords flex" v-if="segment.keywords && segment.keywords.length">
<p v-for="(item, index) in segment.keywords" :key="index" class="mr10">#{{ item }}</p>
</div>
<!-- <span class="segment-chars">{{ segment.characters || 0 }} characters</span>-->
</div>
</div>
<div class="segment-detail" v-if="descriptions.data && descriptions.data.length > 0">
<div class="segment-content" v-if="activeSegment !== null">
{{ descriptions.data[activeSegment].content }}
<div class="flex align-items-c mt20" v-if="descriptions.data[activeSegment].keywords && descriptions.data[activeSegment].keywords.length">
关键词
<el-tag v-for="(item, index) in descriptions.data[activeSegment].keywords" :key="index" class="mr10" size="mini" type="primary">
{{ item }}
</el-tag>
</div>
</div>
<div v-if="activeSegment !== null"></div>
<div class="segment-empty" v-else>
<el-empty description="请选择左侧分段查看详情"></el-empty>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TextModel',
props: {
visible: Boolean,
descriptions: {
type: Object,
default: () => ({ data: [] })
},
documentDetail: {
type: Object,
default: () => ({})
},
activeSegment: {
type: Number,
default: 0
}
}
}
</script>
<style scoped lang="scss">
.segment-split-view {
display: flex;
}
.segment-list {
//flex: 1;
width: 45%;
margin-right: 10px;
}
.segment-list-item {
cursor: pointer;
margin-bottom: 10px;
padding: 15px 0 10px 15px;
border-radius: 15px;
transition: background-color 0.3s;
font-size: 14px;
&:hover {
background: #f3f5f7;
}
&.active {
background: #f3f5f7;
}
p {
margin: 10px 0;
}
.segment-number {
color: #0a84ff;
}
.segment-keywords {
color: #606266;
}
}
.segment-detail {
width: 55%;
}
.segment-content {
padding: 20px;
background: #f9f9f9;
border-radius: 15px;
//white-space: pre-wrap;
line-height: 35px;
color: #666;
}
.segment-empty {
padding: 10px;
text-align: center;
}
</style>

View File

@@ -120,8 +120,7 @@ export default {
})
}
},
created() {
},
created() {},
mounted() {
this.getDetail()
},

View File

@@ -13,16 +13,27 @@
</el-form-item>
<!-- 文件上传-->
<el-form-item label="" required prop="file">
<div @click="createFiled" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop" class="upload-demo" :class="{ 'drag-over': isDragOver }">
<div
@click="createFiled"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
class="upload-demo"
:class="{ 'drag-over': isDragOver }"
>
<el-empty v-if="!filed">
<template #description>
<p>点击或将文件拖拽到这里上传</p>
<p>支持扩展名.xlsx .doc .docx .pdf .txt...</p>
</template>
</el-empty>
<el-result v-else icon="success" title="文件上传成功" :sub-title="`已上传文件:${filed.name}`"
style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<el-result
v-else
icon="success"
title="文件上传成功"
:sub-title="`已上传文件:${filed.name}`"
style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;"
>
<template slot="extra">
<el-button type="primary" size="medium" @click.stop="createFiled">重新上传</el-button>
</template>
@@ -35,8 +46,7 @@
<el-form-item label="是否进行预处理:" required prop="beMinerU">
<template slot="label">
是否进行预处理
<el-tooltip class="item" effect="dark" content="通过整合最先进的文档解析模型来提高内容提取质量"
placement="top">
<el-tooltip class="item" effect="dark" content="通过整合最先进的文档解析模型来提高内容提取质量" placement="top">
<i class="el-icon-info ml5" style="color: #909399;"></i>
</el-tooltip>
@@ -210,10 +220,8 @@ export default {
return true
}
},
created() {
},
mounted() {
},
created() {},
mounted() {},
computed: {}
}
</script>
@@ -249,7 +257,6 @@ export default {
}
}
.preview-container {
display: flex;
height: 500px;

View File

@@ -85,135 +85,19 @@
</div>
</div>
</el-card>
<el-drawer :visible.sync="drawer" size="80%" :title="descriptions.knowledgeName">
<div class="drawer-content">
<div class="file-header">
<div class="file-icon">
<i class="el-icon-document"></i>
</div>
<div class="file-info">
<h3>{{ descriptions.fileName || descriptions.knowledgeName }}</h3>
<el-tag type="primary" class="mr10">{{ getDocumentSourceLabel(descriptions.documentSource) }}</el-tag>
<el-tag type="info">{{ descriptions.createdDate }}</el-tag>
</div>
</div>
<el-divider></el-divider>
<div class="segment-info">
<div class="segment-header">
<h4>分段规则</h4>
</div>
<div class="rule-card">
<div class="rule-item">
<h5>是否使用预处理:</h5>
<span>{{ descriptions.usePreProcess | filterUseMineru }}</span>
</div>
<div class="rule-item">
<h5>是否使用OCR协助处理:</h5>
<span>{{ descriptions.useOcr | filterUseMineru }}</span>
</div>
<div class="rule-item">
<h5>知识拆分规则:</h5>
<span v-if="!documentDetail.splitRules">
-
</span>
<div v-else>
<div v-for="item in documentDetail.splitRules" :key="item.id" class="rule-detail-item">
<p>
<span class="rule-label">样式:</span> <span class="rule-value">{{ item.titleLevel }}</span>
</p>
<p>
<span class="rule-label">关键词:</span> <span class="rule-value">{{ item.ruleRegex }}</span>
</p>
<p>
<span class="rule-label">备注:</span> <span class="rule-value">{{ item.description }}</span>
</p>
</div>
</div>
</div>
<div class="rule-item">
<h5>知识题词规则:</h5>
<span v-if="!documentDetail.extractRules">
-
</span>
<div v-else>
<div v-for="item in documentDetail.extractRules" :key="item.id" class="rule-detail-item">
<p>
<span class="rule-label">属性:</span> <span class="rule-value">{{ item.attribute }}</span>
</p>
<p>
<span class="rule-label">属性描述:</span><span class="rule-value">{{ item.attributeDesc }}</span>
</p>
<p>
<span class="rule-label">关键词:</span> <span class="rule-value">{{ item.keyword }}</span>
</p>
<p>
<span class="rule-label">关键词示例:</span> <span class="rule-value">{{ item.example }}</span>
</p>
<p>
<span class="rule-label">提示词:</span> <span class="rule-value">{{ item.prompt }}</span>
</p>
</div>
</div>
</div>
</div>
</div>
<el-divider></el-divider>
<div class="segment-content-container">
<div class="segment-header mb15">
<h4>分段内容</h4>
<div class="segment-summary">
<span>{{ descriptions.data ? descriptions.data.length : 0 }}个分段</span>
</div>
</div>
<div class="segment-split-view">
<div class="segment-list">
<div
v-for="(segment, index) in descriptions.data"
:key="index"
class="segment-list-item"
:class="{ active: activeSegment === index }"
@click="activeSegment = index"
>
<div>
<span class="segment-number">分段 - {{ index + 1 }}</span> · <span v-if="segment.word_count > 0">{{ segment.word_count }}个字符</span>
</div>
<p>{{ segment.content.slice(0, 20) + '.....' }}</p>
<div class="segment-keywords flex" v-if="segment.keywords && segment.keywords.length">
<p v-for="(item, index) in segment.keywords" :key="index" class="mr10">#{{ item }}</p>
</div>
<!-- <span class="segment-chars">{{ segment.characters || 0 }} characters</span>-->
</div>
</div>
<div class="segment-detail" v-if="descriptions.data && descriptions.data.length > 0">
<div class="segment-content" v-if="activeSegment !== null">
{{ descriptions.data[activeSegment].content }}
<div class="flex align-items-c mt20" v-if="descriptions.data[activeSegment].keywords && descriptions.data[activeSegment].keywords.length">
关键词
<el-tag v-for="(item, index) in descriptions.data[activeSegment].keywords" :key="index" class="mr10" size="mini" type="primary">
{{ item }}
</el-tag>
</div>
</div>
<div v-if="activeSegment !== null"></div>
<div class="segment-empty" v-else>
<el-empty description="请选择左侧分段查看详情"></el-empty>
</div>
</div>
</div>
</div>
</div>
</el-drawer>
<document-drawer
:visible.sync="drawer"
:descriptions="descriptions"
:document-detail="documentDetail"
:active-segment="activeSegment"
@update:visible="val => (drawer = val)"
/>
</div>
</template>
<script>
import { datasetDocumentEx, datasetQueryDelete, datasetQuerySegments, datasetsExPages, datasetUpdate, getDatasetById } from '@/api/generatedApi/index'
import { documentSourceOptions, segmentedModeOptionsMap } from '@/assets/js/utils/utilOptions'
import DocumentDrawer from './components/documentDetail/DocumentDrawer.vue'
export default {
name: 'index',
@@ -251,7 +135,9 @@ export default {
},
props: {},
watch: {},
components: {},
components: {
DocumentDrawer
},
methods: {
// 开启编辑 知识库标题
editKnowledgeName() {
@@ -629,8 +515,9 @@ export default {
margin-top: 8px;
padding: 10px;
background: #f9f9f9;
border-radius: 4px;
border-radius: 8px;
border: 1px solid #e0e0e0;
margin-right: 10px;
p {
margin: 5px 0;

View File

@@ -213,15 +213,25 @@ export default {
<el-form-item label="上传状态">
<el-select v-model="form.documentStatus" placeholder="请选择上传状态">
<el-option label="全部上传状态" value="" />
<el-option v-for="item in Object.keys(this.tableConfig.uploadStatusType)" :key="item"
:label="tableConfig.uploadStatusType[item]" :value="Number(item)" />
<el-option
v-for="item in Object.keys(this.tableConfig.uploadStatusType)"
:key="item"
:label="tableConfig.uploadStatusType[item]"
:value="Number(item)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="任务时间">
<el-date-picker v-model="form.taskTime" type="daterange" unlink-panels range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" />
<el-date-picker
v-model="form.taskTime"
type="daterange"
unlink-panels
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
</el-col>
<el-col :span="9" :offset="11" class="mb20">
@@ -235,14 +245,19 @@ export default {
<!-- 任务列表 -->
<div>
<r-table :columns="tableConfig.columns" :data="tableData" :deletion="false" :total="tableConfig.total"
@currentChange="handleCurrentChange" @sizeChange="handleSizeChange"
:currentPage="tableConfig.currentPage"
:pageSize="tableConfig.pageSize" />
<r-table
:columns="tableConfig.columns"
:data="tableData"
:deletion="false"
:total="tableConfig.total"
@currentChange="handleCurrentChange"
@sizeChange="handleSizeChange"
:currentPage="tableConfig.currentPage"
:pageSize="tableConfig.pageSize"
/>
<!-- 弹出提示框 里面是各种详情内容 -->
<el-drawer title="上传任务详情" :visible.sync="infoDialogVisible" size="60%"
:before-close="() => (infoDialogVisible = false)">
<el-drawer title="上传任务详情" :visible.sync="infoDialogVisible" size="60%" :before-close="() => (infoDialogVisible = false)">
<knowledge-info :form="activeForm" v-if="infoDialogVisible"></knowledge-info>
</el-drawer>
</div>