Pr@main@problem manage (#37)

* feat: 问题模块
This commit is contained in:
shaohuzhang1 2024-04-09 11:33:28 +08:00 committed by GitHub
parent c67f4633a1
commit 633c005905
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1405 additions and 119 deletions

View File

@ -218,7 +218,7 @@ class ParagraphSerializers(ApiMixin, serializers.Serializer):
def association(self, with_valid=True, with_embedding=True): def association(self, with_valid=True, with_embedding=True):
if with_valid: if with_valid:
self.is_valid(raise_exception=True) self.is_valid(raise_exception=True)
problem = QuerySet(Problem).filter(id=self.data.get("problem_id")) problem = QuerySet(Problem).filter(id=self.data.get("problem_id")).first()
problem_paragraph_mapping = ProblemParagraphMapping(id=uuid.uuid1(), problem_paragraph_mapping = ProblemParagraphMapping(id=uuid.uuid1(),
document_id=self.data.get('document_id'), document_id=self.data.get('document_id'),
paragraph_id=self.data.get('paragraph_id'), paragraph_id=self.data.get('paragraph_id'),

View File

@ -8,7 +8,7 @@
""" """
import os import os
import uuid import uuid
from typing import Dict from typing import Dict, List
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import QuerySet
@ -83,6 +83,7 @@ class ProblemSerializers(ApiMixin, serializers.Serializer):
**{'dataset_id': self.data.get('dataset_id')}) **{'dataset_id': self.data.get('dataset_id')})
if 'content' in self.data: if 'content' in self.data:
query_set = query_set.filter(**{'content__contains': self.data.get('content')}) query_set = query_set.filter(**{'content__contains': self.data.get('content')})
query_set = query_set.order_by("-create_time")
return query_set return query_set
def list(self): def list(self):
@ -95,6 +96,22 @@ class ProblemSerializers(ApiMixin, serializers.Serializer):
return native_page_search(current_page, page_size, query_set, select_string=get_file_content( return native_page_search(current_page, page_size, query_set, select_string=get_file_content(
os.path.join(PROJECT_DIR, "apps", "dataset", 'sql', 'list_problem.sql'))) os.path.join(PROJECT_DIR, "apps", "dataset", 'sql', 'list_problem.sql')))
class BatchOperate(serializers.Serializer):
dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("知识库id"))
def delete(self, problem_id_list: List, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
dataset_id = self.data.get('dataset_id')
problem_paragraph_mapping_list = QuerySet(ProblemParagraphMapping).filter(
dataset_id=dataset_id,
problem_id__in=problem_id_list)
source_ids = [row.id for row in problem_paragraph_mapping_list]
problem_paragraph_mapping_list.delete()
QuerySet(Problem).filter(id__in=problem_id_list).delete()
ListenerManagement.delete_embedding_by_source_ids_signal.send(source_ids)
return True
class Operate(serializers.Serializer): class Operate(serializers.Serializer):
dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("知识库id")) dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("知识库id"))
@ -105,6 +122,8 @@ class ProblemSerializers(ApiMixin, serializers.Serializer):
self.is_valid(raise_exception=True) self.is_valid(raise_exception=True)
problem_paragraph_mapping = QuerySet(ProblemParagraphMapping).filter(dataset_id=self.data.get("dataset_id"), problem_paragraph_mapping = QuerySet(ProblemParagraphMapping).filter(dataset_id=self.data.get("dataset_id"),
problem_id=self.data.get("problem_id")) problem_id=self.data.get("problem_id"))
if problem_paragraph_mapping is None or len(problem_paragraph_mapping)==0:
return []
return native_search( return native_search(
QuerySet(Paragraph).filter(id__in=[row.paragraph_id for row in problem_paragraph_mapping]), QuerySet(Paragraph).filter(id__in=[row.paragraph_id for row in problem_paragraph_mapping]),
select_string=get_file_content( select_string=get_file_content(
@ -123,6 +142,7 @@ class ProblemSerializers(ApiMixin, serializers.Serializer):
dataset_id=self.data.get('dataset_id'), dataset_id=self.data.get('dataset_id'),
problem_id=self.data.get('problem_id')) problem_id=self.data.get('problem_id'))
source_ids = [row.id for row in problem_paragraph_mapping_list] source_ids = [row.id for row in problem_paragraph_mapping_list]
problem_paragraph_mapping_list.delete()
QuerySet(Problem).filter(id=self.data.get('problem_id')).delete() QuerySet(Problem).filter(id=self.data.get('problem_id')).delete()
ListenerManagement.delete_embedding_by_source_ids_signal.send(source_ids) ListenerManagement.delete_embedding_by_source_ids_signal.send(source_ids)
return True return True

View File

@ -36,6 +36,25 @@ class ProblemApi(ApiMixin):
} }
) )
class BatchOperate(ApiMixin):
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='dataset_id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='知识库id'),
]
@staticmethod
def get_request_body_api():
return openapi.Schema(
title="问题id列表",
description="问题id列表",
type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING)
)
class Operate(ApiMixin): class Operate(ApiMixin):
@staticmethod @staticmethod
def get_request_params_api(): def get_request_params_api():

View File

@ -36,6 +36,7 @@ urlpatterns = [
'dataset/<str:dataset_id>/document/<str:document_id>/paragraph/<str:paragraph_id>/problem/<str:problem_id>/association', 'dataset/<str:dataset_id>/document/<str:document_id>/paragraph/<str:paragraph_id>/problem/<str:problem_id>/association',
views.Paragraph.Problem.Association.as_view()), views.Paragraph.Problem.Association.as_view()),
path('dataset/<str:dataset_id>/problem', views.Problem.as_view()), path('dataset/<str:dataset_id>/problem', views.Problem.as_view()),
path('dataset/<str:dataset_id>/problem/_batch', views.Problem.OperateBatch.as_view()),
path('dataset/<str:dataset_id>/problem/<int:current_page>/<int:page_size>', views.Problem.Page.as_view()), path('dataset/<str:dataset_id>/problem/<int:current_page>/<int:page_size>', views.Problem.Page.as_view()),
path('dataset/<str:dataset_id>/problem/<str:problem_id>', views.Problem.Operate.as_view()), path('dataset/<str:dataset_id>/problem/<str:problem_id>', views.Problem.Operate.as_view()),
path('dataset/<str:dataset_id>/problem/<str:problem_id>/paragraph', views.Problem.Paragraph.as_view()), path('dataset/<str:dataset_id>/problem/<str:problem_id>/paragraph', views.Problem.Paragraph.as_view()),

View File

@ -51,7 +51,7 @@ class Problem(APIView):
def post(self, request: Request, dataset_id: str): def post(self, request: Request, dataset_id: str):
return result.success( return result.success(
ProblemSerializers.Create( ProblemSerializers.Create(
data={'dataset_id': dataset_id, 'problem_list': request.query_params.get('problem_list')}).save()) data={'dataset_id': dataset_id, 'problem_list': request.data}).batch())
class Paragraph(APIView): class Paragraph(APIView):
authentication_classes = [TokenAuth] authentication_classes = [TokenAuth]
@ -70,6 +70,24 @@ class Problem(APIView):
data={**query_params_to_single_dict(request.query_params), 'dataset_id': dataset_id, data={**query_params_to_single_dict(request.query_params), 'dataset_id': dataset_id,
'problem_id': problem_id}).list_paragraph()) 'problem_id': problem_id}).list_paragraph())
class OperateBatch(APIView):
authentication_classes = [TokenAuth]
@action(methods=['DELETE'], detail=False)
@swagger_auto_schema(operation_summary="批量删除问题",
operation_id="批量删除问题",
request_body=
ProblemApi.BatchOperate.get_request_body_api(),
manual_parameters=ProblemApi.BatchOperate.get_request_params_api(),
responses=result.get_default_response(),
tags=["知识库/文档/段落/问题"])
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
def delete(self, request: Request, dataset_id: str):
return result.success(
ProblemSerializers.BatchOperate(data={'dataset_id': dataset_id}).delete(request.data))
class Operate(APIView): class Operate(APIView):
authentication_classes = [TokenAuth] authentication_classes = [TokenAuth]

View File

@ -129,25 +129,55 @@ const postProblem: (
dataset_id: string, dataset_id: string,
document_id: string, document_id: string,
paragraph_id: string, paragraph_id: string,
data: any data: any,
) => Promise<Result<any>> = (dataset_id, document_id, paragraph_id, data: any) => { loading?: Ref<boolean>
) => Promise<Result<any>> = (dataset_id, document_id, paragraph_id, data: any, loading) => {
return post( return post(
`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem`, `${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem`,
data data,
{},
loading
)
}
/**
*
* @param dataset_id id
* @param document_id id
* @param paragraph_id id
* @param problem_id id
* @param loading
* @returns
*/
const associationProblem: (
dataset_id: string,
document_id: string,
paragraph_id: string,
problem_id: string,
loading?: Ref<boolean>
) => Promise<Result<any>> = (dataset_id, document_id, paragraph_id, problem_id, loading) => {
return put(
`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem/${problem_id}/association`,
{},
{},
loading
) )
} }
/** /**
* *
* @param dataset_id, document_id, paragraph_id,problem_id * @param dataset_id, document_id, paragraph_id,problem_id
*/ */
const delProblem: ( const disassociationProblem: (
dataset_id: string, dataset_id: string,
document_id: string, document_id: string,
paragraph_id: string, paragraph_id: string,
problem_id: string problem_id: string,
) => Promise<Result<boolean>> = (dataset_id, document_id, paragraph_id, problem_id) => { loading?: Ref<boolean>
) => Promise<Result<boolean>> = (dataset_id, document_id, paragraph_id, problem_id, loading) => {
return put( return put(
`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem/${problem_id}/un_association` `${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem/${problem_id}/un_association`,
{},
{},
loading
) )
} }
@ -158,5 +188,6 @@ export default {
postParagraph, postParagraph,
getProblem, getProblem,
postProblem, postProblem,
delProblem disassociationProblem,
associationProblem
} }

107
ui/src/api/problem.ts Normal file
View File

@ -0,0 +1,107 @@
import { Result } from '@/request/Result'
import { get, post, del, put } from '@/request/index'
import type { Ref } from 'vue'
import type { KeyValue } from '@/api/type/common'
import type { pageRequest } from '@/api/type/common'
const prefix = '/dataset'
/**
*
* @param dataset_id,
* page {
"current_page": "string",
"page_size": "string",
}
* query {
"content": "string",
}
*/
const getProblems: (
dataset_id: string,
page: pageRequest,
param: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (dataset_id, page, param, loading) => {
return get(
`${prefix}/${dataset_id}/problem/${page.current_page}/${page.page_size}`,
param,
loading
)
}
/**
*
* @param dataset_id
* data: array[string]
*/
const postProblems: (
dataset_id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (dataset_id, data, loading) => {
return post(`${prefix}/${dataset_id}/problem`, data, undefined, loading)
}
/**
*
* @param dataset_id, problem_id,
*/
const delProblems: (
dataset_id: string,
problem_id: string,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (dataset_id, problem_id, loading) => {
return del(`${prefix}/${dataset_id}/problem/${problem_id}`, loading)
}
/**
*
* @param dataset_id,
*/
const delMulProblem: (
dataset_id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (dataset_id, data, loading) => {
return del(`${prefix}/${dataset_id}/problem/_batch`, undefined, data, loading)
}
/**
*
* @param
* dataset_id, problem_id,
* {
"content": "string",
}
*/
const putProblems: (
dataset_id: string,
problem_id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (dataset_id, problem_id, data: any, loading) => {
return put(`${prefix}/${dataset_id}/problem/${problem_id}`, data, undefined, loading)
}
/**
*
* @param
* dataset_id, problem_id,
*/
const getDetailProblems: (
dataset_id: string,
problem_id: string,
loading?: Ref<boolean>
) => Promise<Result<any>> = (dataset_id, problem_id, loading) => {
return get(`${prefix}/${dataset_id}/problem/${problem_id}/paragraph`, undefined, loading)
}
export default {
getProblems,
postProblems,
delProblems,
putProblems,
getDetailProblems,
delMulProblem
}

View File

@ -102,13 +102,4 @@ defineExpose({ open })
height: calc(100vh - 260px); height: calc(100vh - 260px);
} }
} }
.paragraph-source-card {
height: 210px;
width: 100%;
.active-button {
position: absolute;
right: 16px;
top: 16px;
}
}
</style> </style>

View File

@ -6,9 +6,11 @@
<el-input <el-input
ref="quickInputRef" ref="quickInputRef"
v-model="inputValue" v-model="inputValue"
placeholder="请输入文档名称" :placeholder="`请输入${quickCreateName}`"
class="w-500 mr-12" class="w-500 mr-12"
autofocus autofocus
:maxlength="quickCreateMaxlength"
:show-word-limit="quickCreateMaxlength ? true : false"
/> />
<el-button type="primary" @click="submitHandle" :disabled="loading">创建</el-button> <el-button type="primary" @click="submitHandle" :disabled="loading">创建</el-button>
@ -17,7 +19,7 @@
<div v-else @click="quickCreateHandel" class="w-full"> <div v-else @click="quickCreateHandel" class="w-full">
<el-button type="primary" link class="quich-button"> <el-button type="primary" link class="quich-button">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
<span class="ml-4">快速创建空白文档</span> <span class="ml-4">{{ quickCreatePlaceholder }}</span>
</el-button> </el-button>
</div> </div>
</template> </template>
@ -51,6 +53,18 @@ const props = defineProps({
quickCreate: { quickCreate: {
type: Boolean, type: Boolean,
default: false default: false
},
quickCreateName: {
type: String,
default: '文档名称'
},
quickCreatePlaceholder: {
type: String,
default: '快速创建空白文档'
},
quickCreateMaxlength: {
type: Number,
default: () => 0
} }
}) })
const emit = defineEmits(['changePage', 'sizeChange', 'creatQuick']) const emit = defineEmits(['changePage', 'sizeChange', 'creatQuick'])
@ -81,7 +95,7 @@ function submitHandle() {
loading.value = false loading.value = false
}, 200) }, 200)
} else { } else {
MsgError('文件名称不能为空!') MsgError(`${props.quickCreateName}不能为空!`)
} }
} }

View File

@ -728,5 +728,51 @@ export const iconMap: any = {
) )
]) ])
} }
},
'app-problems': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 1024 1024',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M565.03091564 528.45078523a588.83471385 588.83471385 0 0 1 16.90811971-15.58251253c16.81532721-14.88656875 34.70439623-28.84521246 50.33330501-44.73261462 28.33485369-28.83195638 37.04409293-63.99368709 29.02416942-101.57465094-9.23948212-43.27444672-40.20566608-71.52976398-84.66653122-81.02111147-31.27770165-8.21876458-35.38708395-7.01909007-67.9373685-4.33473551-37.94550581 6.16407344-39.35727747 6.05802485-76.22241344 22.62811474-2.48551348 1.39188755-19.28758462 10.35962019-24.5966414 15.11855-11.44661809 5.60731841-19.40026124 17.25940562-19.40026123 30.86013539a34.46578693 34.46578693 0 0 0 34.46578694 34.46578694 34.1807814 34.1807814 0 0 0 20.83854503-7.17816293c0.35128591-0.22535322 0.69594378-0.41756626 1.06711379-0.74896807 28.77230406-25.79631593 62.90668921-36.7259472 102.56885634-31.38375021 15.43006769 2.07457524 28.54032281 8.45737387 38.05818242 20.42097876 12.23535436 15.3770434 10.79707056 32.51714437 6.85338917 49.71026962-3.05552458 13.30909618-11.26103308 24.31163586-21.66704951 33.43181333-17.02079632 14.932965-34.65799999 29.27603478-52.28194758 43.60584853-19.63224249 15.97356663-28.85846852 36.7259472-31.52293898 60.18919446a257.89025081 257.89025081 0 0 0-1.49793613 30.30338037h0.04639624c-0.03976821 19.12188371 16.21880398 32.68947331 30.90653165 33.12029565 20.02329661 0.59652323 35.11533446-13.47479709 35.32743162-32.39783973-0.00662803-1.0869979-0.19884108-2.07457524-0.28500555-3.12180494-0.00662803-5.1433559-0.0927925-10.29333983 0.01988411-15.43006769 0.29826162-13.49468121 3.10854885-26.22713825 13.66038209-36.34814915zM515.93042532 643.75209862c-19.01583514-0.76222413-32.4309799 15.15169019-33.41192923 31.12525684-1.23281469 20.1691134 15.69518913 34.65799999 30.89327557 35.10870642 20.02329661 0.59652323 35.11533446-13.47479709 35.32743161-32.39783973-0.13918876-19.99015643-13.38863262-33.06064333-32.80877795-33.83612353zM96.72703555 251.52481518h120.80258323c17.31242991 0 31.34398202-14.84017249 31.34398202-33.14017976s-14.03818015-33.14017975-31.34398202-33.14017975H96.72703555c-17.31242991 0-31.34398202 14.84017249-31.34398201 33.14017975s14.03155212 33.14017975 31.34398201 33.14017976zM94.63920422 412.78492985h120.80258324c17.31242991 0 31.34398202-14.84017249 31.34398201-33.14017974s-14.03818015-33.14017975-31.34398201-33.14017976H94.63920422c-17.31242991 0-31.35061005 14.84017249-31.35061003 33.14017976s14.03818015 33.14017975 31.35061003 33.14017974zM246.78576947 542.32989251c0-18.3066353-14.03818015-33.14017975-31.34398201-33.14017975H94.63920422c-17.31242991 0-31.35061005 14.83354446-31.35061003 33.14017975 0 18.30000725 14.03818015 33.14017975 31.35061003 33.14017976h120.80258324c17.30580187 0 31.34398202-14.84017249 31.34398201-33.14017976z',
fill: 'currentColor'
}),
h('path', {
d: 'M824.35945025 44.76986174H194.99429654a35.93058289 35.93058289 0 0 0 0 71.84790971h629.36515371c19.80457142 0 35.96372307 16.13263951 35.96372307 35.93058289v718.5652615a35.99023521 35.99023521 0 0 1-35.96372307 35.93721092H230.10963102a35.95709503 35.95709503 0 0 1-35.95709503-35.93721092v-190.42347285a35.93721092 35.93721092 0 0 0-35.96372307-35.92395486 35.92395486 35.92395486 0 0 0-35.95709503 35.92395486v190.42347285c0 59.43359837 48.40454655 107.79837669 107.87791313 107.7983767h594.24981923c59.47999461 0 107.8712851-48.36477833 107.87128509-107.7983767V152.55498237c0-59.42697034-48.39129049-107.78512063-107.87128509-107.78512063z',
fill: 'currentColor'
})
]
)
])
}
},
'app-quxiaoguanlian': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 1024 1024',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M544 298.688a32 32 0 0 1 32-32h320c41.216 0 74.688 33.408 74.688 74.624V640c0 41.216-33.472 74.688-74.688 74.688h-85.312a32 32 0 1 1 0-64H896a10.688 10.688 0 0 0 10.688-10.688V341.312A10.688 10.688 0 0 0 896 330.688H576a32 32 0 0 1-32-32zM53.312 341.312c0-41.216 33.472-74.624 74.688-74.624h106.688a32 32 0 1 1 0 64H128a10.688 10.688 0 0 0-10.688 10.624V640c0 5.888 4.8 10.688 10.688 10.688h320a32 32 0 1 1 0 64H128A74.688 74.688 0 0 1 53.312 640V341.312zM282.432 100.416a32 32 0 0 1 43.84 11.392l426.624 725.312a32 32 0 0 1-55.168 32.448L271.104 144.256a32 32 0 0 1 11.328-43.84zM650.688 490.688a32 32 0 0 1 32-32H768a32 32 0 1 1 0 64h-85.312a32 32 0 0 1-32-32zM224 490.688a32 32 0 0 1 32-32h85.312a32 32 0 1 1 0 64H256a32 32 0 0 1-32-32z',
fill: 'currentColor'
})
]
)
])
}
} }
} }

View File

@ -1,20 +1,27 @@
<template> <template>
<div class="cursor"> <div class="cursor w-full">
<slot name="read"> <slot name="read">
<div class="flex align-center" v-if="!isEdit"> <div class="flex align-center" v-if="!isEdit">
<auto-tooltip :content="data"> <auto-tooltip :content="data">
{{ data }} {{ data }}
</auto-tooltip> </auto-tooltip>
<el-button @click.stop="editNameHandle" text v-if="showEditIcon"> <el-button class="ml-4" @click.stop="editNameHandle" text v-if="showEditIcon">
<el-icon><Edit /></el-icon> <el-icon><EditPen /></el-icon>
</el-button> </el-button>
</div> </div>
</slot> </slot>
<slot> <slot>
<div class="flex align-center" v-if="isEdit"> <div class="flex align-center" @click.stop v-if="isEdit">
<div @click.stop> <div class="w-full">
<el-input ref="inputRef" v-model="writeValue" placeholder="请输入" autofocus></el-input> <el-input
ref="inputRef"
v-model="writeValue"
placeholder="请输入"
autofocus
:maxlength="maxlength"
:show-word-limit="maxlength ? true : false"
></el-input>
</div> </div>
<span class="ml-4"> <span class="ml-4">
@ -42,6 +49,10 @@ const props = defineProps({
showEditIcon: { showEditIcon: {
type: Boolean, type: Boolean,
default: false default: false
},
maxlength: {
type: Number,
default: () => 0
} }
}) })
const emit = defineEmits(['change']) const emit = defineEmits(['change'])

View File

@ -37,6 +37,18 @@ const datasetRouter = {
}, },
component: () => import('@/views/document/index.vue') component: () => import('@/views/document/index.vue')
}, },
{
path: 'problem',
name: 'Problem',
meta: {
icon: 'app-problems',
title: '问题',
active: 'problem',
parentPath: '/dataset/:id',
parentName: 'DatasetDetail'
},
component: () => import('@/views/problem/index.vue')
},
{ {
path: 'hit-test', path: 'hit-test',
name: 'DatasetHitTest', name: 'DatasetHitTest',

View File

@ -8,6 +8,7 @@ import useParagraphStore from './modules/paragraph'
import useModelStore from './modules/model' import useModelStore from './modules/model'
import useApplicationStore from './modules/application' import useApplicationStore from './modules/application'
import useDocumentStore from './modules/document' import useDocumentStore from './modules/document'
import useProblemStore from './modules/problem'
const useStore = () => ({ const useStore = () => ({
common: useCommonStore(), common: useCommonStore(),
@ -16,7 +17,8 @@ const useStore = () => ({
paragraph: useParagraphStore(), paragraph: useParagraphStore(),
model: useModelStore(), model: useModelStore(),
application: useApplicationStore(), application: useApplicationStore(),
document: useDocumentStore() document: useDocumentStore(),
problem: useProblemStore()
}) })
export default useStore export default useStore

View File

@ -3,7 +3,7 @@ import documentApi from '@/api/document'
import { type Ref } from 'vue' import { type Ref } from 'vue'
const useDocumentStore = defineStore({ const useDocumentStore = defineStore({
id: 'documents', id: 'document',
state: () => ({}), state: () => ({}),
actions: { actions: {
async asyncGetAllDocument(id: string, loading?: Ref<boolean>) { async asyncGetAllDocument(id: string, loading?: Ref<boolean>) {
@ -17,6 +17,18 @@ const useDocumentStore = defineStore({
reject(error) reject(error)
}) })
}) })
},
async asyncPostDocument(datasetId: string, data: any, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
documentApi
.postDocument(datasetId, data, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
} }
} }
}) })

View File

@ -0,0 +1,79 @@
import { defineStore } from 'pinia'
import { type Ref } from 'vue'
import problemApi from '@/api/problem'
import paragraphApi from '@/api/paragraph'
import type { pageRequest } from '@/api/type/common'
const useProblemStore = defineStore({
id: 'problem',
state: () => ({}),
actions: {
async asyncPostProblem(datasetId: string, data: any, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
problemApi
.postProblems(datasetId, data, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncGetProblem(
datasetId: string,
page: pageRequest,
param: any,
loading?: Ref<boolean>
) {
return new Promise((resolve, reject) => {
problemApi
.getProblems(datasetId, page, param, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncDisassociationProblem(
datasetId: string,
documentId: string,
paragraphId: string,
problemId: string,
loading?: Ref<boolean>
) {
return new Promise((resolve, reject) => {
paragraphApi
.disassociationProblem(datasetId, documentId, paragraphId, problemId, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncAssociationProblem(
datasetId: string,
documentId: string,
paragraphId: string,
problemId: string,
loading?: Ref<boolean>
) {
return new Promise((resolve, reject) => {
paragraphApi
.associationProblem(datasetId, documentId, paragraphId, problemId, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
}
}
})
export default useProblemStore

View File

@ -533,3 +533,36 @@ h4 {
} }
} }
} }
// 段落card
.paragraph-source-card {
height: 210px;
width: 100%;
.active-button {
position: absolute;
right: 16px;
top: 16px;
}
}
// 分段 dialog
.paragraph-dialog {
padding: 0 !important;
.el-scrollbar {
height: auto !important;
}
.el-dialog__header {
padding: 16px 24px;
}
.el-dialog__body {
border-top: 1px solid var(--el-border-color);
}
.el-dialog__footer {
padding: 16px 24px;
border-top: 1px solid var(--el-border-color);
}
.title {
color: var(--app-text-color);
}
}

View File

@ -58,18 +58,18 @@ import StepSecond from './step/StepSecond.vue'
import ResultSuccess from './step/ResultSuccess.vue' import ResultSuccess from './step/ResultSuccess.vue'
import datasetApi from '@/api/dataset' import datasetApi from '@/api/dataset'
import type { datasetData } from '@/api/type/dataset' import type { datasetData } from '@/api/type/dataset'
import documentApi from '@/api/document'
import { MsgConfirm, MsgSuccess } from '@/utils/message' import { MsgConfirm, MsgSuccess } from '@/utils/message'
import useStore from '@/stores' import useStore from '@/stores'
const { dataset } = useStore() const { dataset, document } = useStore()
const baseInfo = computed(() => dataset.baseInfo) const baseInfo = computed(() => dataset.baseInfo)
const webInfo = computed(() => dataset.webInfo) const webInfo = computed(() => dataset.webInfo)
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { const {
params: { id, type } params: { type },
query: { id } // iddatasetIDid
} = route } = route
const isCreate = type === 'create' const isCreate = type === 'create'
// const steps = [ // const steps = [
@ -112,19 +112,19 @@ function clearStore() {
} }
function submit() { function submit() {
loading.value = true loading.value = true
const documents = [] as any[] const data = [] as any
StepSecondRef.value?.paragraphList.map((item: any) => { StepSecondRef.value?.paragraphList.map((item: any) => {
documents.push({ data.push({
name: item.name, name: item.name,
paragraphs: item.content paragraphs: item.content
}) })
}) })
const obj = { ...baseInfo.value, documents } as datasetData const obj = { ...baseInfo.value, data } as datasetData
const id = route.query.id
if (id) { if (id) {
documentApi //
.postDocument(id as string, documents) document
.then((res) => { .asyncPostDocument(id as string, data)
.then(() => {
MsgSuccess('提交成功') MsgSuccess('提交成功')
clearStore() clearStore()
router.push({ path: `/dataset/${id}/document` }) router.push({ path: `/dataset/${id}/document` })

View File

@ -10,14 +10,14 @@
<p class="mb-8">同步方式</p> <p class="mb-8">同步方式</p>
<el-radio-group v-model="method" class="card__radio"> <el-radio-group v-model="method" class="card__radio">
<el-card shadow="never" class="mb-16" :class="method === 'replace' ? 'active' : ''"> <el-card shadow="never" class="mb-16" :class="method === 'replace' ? 'active' : ''">
<el-radio label="replace" size="large"> <el-radio value="replace" size="large">
<p class="mb-4">替换同步</p> <p class="mb-4">替换同步</p>
<el-text type="info">重新获取 Web 站点文档覆盖替换本地知识库中的文档</el-text> <el-text type="info">重新获取 Web 站点文档覆盖替换本地知识库中的文档</el-text>
</el-radio> </el-radio>
</el-card> </el-card>
<el-card shadow="never" class="mb-16" :class="method === 'complete' ? 'active' : ''"> <el-card shadow="never" class="mb-16" :class="method === 'complete' ? 'active' : ''">
<el-radio label="complete" size="large"> <el-radio value="complete" size="large">
<p class="mb-4">整体同步</p> <p class="mb-4">整体同步</p>
<el-text type="info">先删除本地知识库所有文档重新获取 Web 站点文档</el-text> <el-text type="info">先删除本地知识库所有文档重新获取 Web 站点文档</el-text>
</el-radio> </el-radio>

View File

@ -16,7 +16,7 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-card shadow="never" class="mb-16" :class="form.type === '0' ? 'active' : ''"> <el-card shadow="never" class="mb-16" :class="form.type === '0' ? 'active' : ''">
<el-radio label="0" size="large"> <el-radio value="0" size="large">
<div class="flex align-center"> <div class="flex align-center">
<AppAvatar class="mr-8" shape="square" :size="32"> <AppAvatar class="mr-8" shape="square" :size="32">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" /> <img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
@ -31,7 +31,7 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-card shadow="never" class="mb-16" :class="form.type === '1' ? 'active' : ''"> <el-card shadow="never" class="mb-16" :class="form.type === '1' ? 'active' : ''">
<el-radio label="1" size="large"> <el-radio value="1" size="large">
<div class="flex align-center"> <div class="flex align-center">
<AppAvatar class="mr-8 avatar-purple" shape="square" :size="32"> <AppAvatar class="mr-8 avatar-purple" shape="square" :size="32">
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" /> <img src="@/assets/icon_web.svg" style="width: 58%" alt="" />

View File

@ -8,13 +8,13 @@
<div class="left-height" @click.stop> <div class="left-height" @click.stop>
<el-radio-group v-model="radio" class="set-rules__radio"> <el-radio-group v-model="radio" class="set-rules__radio">
<el-card shadow="never" class="mb-16" :class="radio === '1' ? 'active' : ''"> <el-card shadow="never" class="mb-16" :class="radio === '1' ? 'active' : ''">
<el-radio label="1" size="large"> <el-radio value="1" size="large">
<p class="mb-4">智能分段推荐)</p> <p class="mb-4">智能分段推荐)</p>
<el-text type="info">不了解如何设置分段规则推荐使用智能分段</el-text> <el-text type="info">不了解如何设置分段规则推荐使用智能分段</el-text>
</el-radio> </el-radio>
</el-card> </el-card>
<el-card shadow="never" class="mb-16" :class="radio === '2' ? 'active' : ''"> <el-card shadow="never" class="mb-16" :class="radio === '2' ? 'active' : ''">
<el-radio label="2" size="large"> <el-radio value="2" size="large">
<p class="mb-4">高级分段</p> <p class="mb-4">高级分段</p>
<el-text type="info" <el-text type="info"
>用户可根据文档规范自行设置分段标识符分段长度以及清洗规则 >用户可根据文档规范自行设置分段标识符分段长度以及清洗规则

View File

@ -169,10 +169,10 @@ import useStore from '@/stores'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { const {
params: { id } params: { id } // iddatasetID
} = route as any } = route as any
const { dataset } = useStore() const { dataset, document } = useStore()
const SyncWebDialogRef = ref() const SyncWebDialogRef = ref()
const loading = ref(false) const loading = ref(false)
@ -262,9 +262,9 @@ function rowClickHandle(row: any) {
function creatQuickHandle(val: string) { function creatQuickHandle(val: string) {
loading.value = true loading.value = true
const obj = [{ name: val }] const obj = [{ name: val }]
documentApi document
.postDocument(id, obj) .asyncPostDocument(id, obj)
.then((res) => { .then(() => {
getList() getList()
MsgSuccess('创建成功') MsgSuccess('创建成功')
}) })

View File

@ -27,7 +27,7 @@
</el-button> </el-button>
</div> </div>
</el-col> </el-col>
<el-col :span="6" class="border-l"> <el-col :span="6" class="border-l" style="width: 300px;">
<!-- 关联问题 --> <!-- 关联问题 -->
<ProblemComponent <ProblemComponent
:problemId="problemId" :problemId="problemId"
@ -150,24 +150,5 @@ const handleDebounceClick = debounce(() => {
defineExpose({ open }) defineExpose({ open })
</script> </script>
<style lang="scss" scope> <style lang="scss" scope>
.paragraph-dialog {
padding: 0 !important;
.el-scrollbar {
height: auto !important;
}
.el-dialog__header {
padding: 16px 24px;
}
.el-dialog__body {
border-top: 1px solid var(--el-border-color);
}
.el-dialog__footer {
padding: 16px 24px;
border-top: 1px solid var(--el-border-color);
}
.title {
color: var(--app-text-color);
}
}
</style> </style>

View File

@ -11,16 +11,7 @@
<div v-loading="loading"> <div v-loading="loading">
<el-scrollbar height="345px"> <el-scrollbar height="345px">
<div class="p-24" style="padding-top: 16px"> <div class="p-24" style="padding-top: 16px">
<el-input <el-select
ref="inputRef"
v-if="isAddProblem"
v-model="problemValue"
@change="addProblemHandle"
placeholder="请输入问题,回车保存"
class="mb-8"
autofocus
/>
<!-- <el-select
v-if="isAddProblem" v-if="isAddProblem"
v-model="problemValue" v-model="problemValue"
filterable filterable
@ -28,15 +19,19 @@
default-first-option default-first-option
:reserve-keyword="false" :reserve-keyword="false"
placeholder="请选择问题" placeholder="请选择问题"
style="width: 240px" remote
:remote-method="remoteMethod"
:loading="optionLoading"
@change="addProblemHandle"
class="mb-16"
> >
<el-option <el-option
v-for="item in problemList" v-for="item in problemOptions"
:key="item.value" :key="item.id"
:label="item.label" :label="item.content"
:value="item.value" :value="item.id"
/> />
</el-select> --> </el-select>
<template v-for="(item, index) in problemList" :key="index"> <template v-for="(item, index) in problemList" :key="index">
<TagEllipsis <TagEllipsis
@close="delProblemHandle(item, index)" @close="delProblemHandle(item, index)"
@ -56,6 +51,7 @@
import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue' import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import paragraphApi from '@/api/paragraph' import paragraphApi from '@/api/paragraph'
import useStore from '@/stores'
const props = defineProps({ const props = defineProps({
problemId: String, problemId: String,
@ -65,9 +61,10 @@ const props = defineProps({
const route = useRoute() const route = useRoute()
const { const {
params: { id, documentId } params: { id, documentId } // iddatasetId
} = route as any } = route as any
const { problem } = useStore()
const inputRef = ref() const inputRef = ref()
const loading = ref(false) const loading = ref(false)
const isAddProblem = ref(false) const isAddProblem = ref(false)
@ -75,6 +72,9 @@ const isAddProblem = ref(false)
const problemValue = ref('') const problemValue = ref('')
const problemList = ref<any[]>([]) const problemList = ref<any[]>([])
const problemOptions = ref<any[]>([])
const optionLoading = ref(false)
watch( watch(
() => props.problemId, () => props.problemId,
(value) => { (value) => {
@ -88,19 +88,20 @@ watch(
) )
function delProblemHandle(item: any, index: number) { function delProblemHandle(item: any, index: number) {
loading.value = true
if (item.id) { if (item.id) {
paragraphApi problem
.delProblem(props.datasetId || id, documentId || props.docId, props.problemId || '', item.id) .asyncDisassociationProblem(
.then((res) => { props.datasetId || id,
documentId || props.docId,
props.problemId || '',
item.id,
loading
)
.then((res: any) => {
getProblemList() getProblemList()
}) })
.catch(() => {
loading.value = false
})
} else { } else {
problemList.value.splice(index, 1) problemList.value.splice(index, 1)
loading.value = false
} }
} }
@ -124,32 +125,52 @@ function addProblem() {
}) })
} }
function addProblemHandle(val: string) { function addProblemHandle(val: string) {
if (val) { if (props.problemId) {
const obj = { const api = problemOptions.value.some((option) => option.id === val)
content: val ? problem.asyncAssociationProblem(
} props.datasetId || id,
loading.value = true documentId || props.docId,
if (props.problemId) { props.problemId,
paragraphApi val,
.postProblem(props.datasetId || id, documentId || props.docId, props.problemId, obj) loading
.then((res) => { )
getProblemList() : paragraphApi.postProblem(
problemValue.value = '' props.datasetId || id,
isAddProblem.value = false documentId || props.docId,
}) props.problemId,
.catch(() => { {
loading.value = false content: val
}) },
} else { loading
problemList.value.unshift(obj) )
api.then(() => {
getProblemList()
problemValue.value = '' problemValue.value = ''
isAddProblem.value = false isAddProblem.value = false
loading.value = false })
}
} }
} }
onMounted(() => {}) const remoteMethod = (query: string) => {
getProblemOption(query)
}
function getProblemOption(filterText?: string) {
return problem
.asyncGetProblem(
id as string,
{ current_page: 1, page_size: 100 },
filterText && { content: filterText },
optionLoading
)
.then((res: any) => {
problemOptions.value = res.data.records
})
}
onMounted(() => {
getProblemOption()
})
onUnmounted(() => { onUnmounted(() => {
problemList.value = [] problemList.value = []
problemValue.value = '' problemValue.value = ''
@ -162,6 +183,6 @@ defineExpose({
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.question-tag { .question-tag {
width: 217px; // width: 217px;
} }
</style> </style>

View File

@ -0,0 +1,90 @@
<template>
<el-dialog
title="创建问题"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
>
<el-form
label-position="top"
ref="problemFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item label="问题" prop="data">
<el-input
v-model="form.data"
placeholder="请输入问题,支持输入多个,一行一个。"
:rows="10"
type="textarea"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="submit(problemFormRef)" :loading="loading">
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import { MsgSuccess } from '@/utils/message'
import useStore from '@/stores'
const route = useRoute()
const {
params: { id }
} = route as any
const { problem } = useStore()
const emit = defineEmits(['refresh'])
const problemFormRef = ref()
const loading = ref<boolean>(false)
const form = ref<any>({
data: ''
})
const rules = reactive({
data: [{ required: true, message: '请输入问题', trigger: 'blur' }]
})
const dialogVisible = ref<boolean>(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
data: ''
}
}
})
const open = () => {
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
const arr = form.value.data.split('\n')
problem.asyncPostProblem(id, arr, loading).then((res: any) => {
MsgSuccess('创建成功')
emit('refresh')
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,197 @@
<template>
<el-drawer v-model="visible" size="60%" @close="closeHandel">
<template #header>
<h4>问题详情</h4>
</template>
<div>
<el-scrollbar>
<div class="p-8">
<el-form label-position="top" v-loading="loading">
<el-form-item label="问题">
<ReadWrite
@change="editName"
:data="currentContent"
:showEditIcon="true"
:maxlength="256"
/>
</el-form-item>
<el-form-item label="关联分段">
<template v-for="(item, index) in paragraphList" :key="index">
<CardBox
shadow="never"
:title="item.title || '-'"
class="paragraph-source-card cursor mb-8"
:showIcon="false"
>
<div class="active-button">
<span class="mr-4">
<el-tooltip effect="dark" content="编辑" placement="top">
<el-button type="primary" text @click.stop="editParagraph(item)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip effect="dark" content="取消关联" placement="top">
<el-button type="primary" text @click.stop="disassociation(item)">
<AppIcon iconName="app-quxiaoguanlian"></AppIcon>
</el-button>
</el-tooltip>
</span>
</div>
<template #description>
<el-scrollbar height="80">
{{ item.content }}
</el-scrollbar>
</template>
<template #footer>
<div class="footer-content flex-between">
<el-text>
<el-icon>
<Document />
</el-icon>
{{ item?.document_name }}
</el-text>
</div>
</template>
</CardBox>
</template>
</el-form-item>
</el-form>
</div>
</el-scrollbar>
<ParagraphDialog ref="ParagraphDialogRef" title="编辑分段" @refresh="refresh" />
<RelateProblemDialog ref="RelateProblemDialogRef" @refresh="refresh" />
</div>
<template #footer>
<div>
<el-button @click="relateProblem">关联分段</el-button>
<el-button @click="pre" :disabled="pre_disable || loading">上一条</el-button>
<el-button @click="next" :disabled="next_disable || loading">下一条</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import problemApi from '@/api/problem'
import ParagraphDialog from '@/views/paragraph/component/ParagraphDialog.vue'
import RelateProblemDialog from './RelateProblemDialog.vue'
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
import useStore from '@/stores'
const props = withDefaults(
defineProps<{
/**
* 当前的id
*/
currentId: string
currentContent: string
/**
* 下一条
*/
next: () => void
/**
* 上一条
*/
pre: () => void
pre_disable: boolean
next_disable: boolean
}>(),
{}
)
const emit = defineEmits(['update:currentId', 'update:currentContent', 'refresh'])
const route = useRoute()
const {
params: { id }
} = route
const { problem } = useStore()
const RelateProblemDialogRef = ref()
const ParagraphDialogRef = ref()
const loading = ref(false)
const visible = ref(false)
const paragraphList = ref<any[]>([])
function disassociation(item: any) {
problem
.asyncDisassociationProblem(
item.dataset_id,
item.document_id,
item.id,
props.currentId,
loading
)
.then(() => {
getRecord()
})
}
function relateProblem() {
RelateProblemDialogRef.value.open(props.currentId)
}
function editParagraph(row: any) {
ParagraphDialogRef.value.open(row)
}
function editName(val: string) {
if (val) {
const obj = {
content: val
}
problemApi.putProblems(id as string, props.currentId, obj, loading).then(() => {
emit('update:currentContent', val)
MsgSuccess('修改成功')
})
} else {
MsgError('问题不能为空!')
}
}
function closeHandel() {
paragraphList.value = []
}
function getRecord() {
if (props.currentId && visible.value) {
problemApi.getDetailProblems(id as string, props.currentId, loading).then((res) => {
paragraphList.value = res.data
})
}
}
function refresh() {
getRecord()
}
watch(
() => props.currentId,
() => {
paragraphList.value = []
getRecord()
}
)
watch(visible, (bool) => {
if (!bool) {
emit('update:currentId', '')
emit('update:currentContent', '')
emit('refresh')
}
})
const open = () => {
getRecord()
visible.value = true
}
defineExpose({
open
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,261 @@
<template>
<el-dialog
title="关联分段"
v-model="dialogVisible"
width="80%"
class="paragraph-dialog"
destroy-on-close
>
<el-row v-loading="loading">
<el-col :span="6">
<el-scrollbar height="500" wrap-class="paragraph-scrollbar">
<div class="bold title align-center p-24 pb-0">选择文档</div>
<div class="p-8" style="padding-bottom: 8px">
<common-list
:data="documentList"
class="mt-8"
@click="clickDocumentHandle"
:default-active="currentDocument"
>
<template #default="{ row }">
<span class="flex lighter align-center">
<auto-tooltip :content="row.name">
{{ row.name }}
</auto-tooltip>
<el-badge
:value="associationCount(row.id)"
type="primary"
v-if="associationCount(row.id)"
class="paragraph-badge ml-4"
/>
</span>
</template>
</common-list>
</div>
</el-scrollbar>
</el-col>
<el-col :span="18" class="border-l">
<el-scrollbar height="500" wrap-class="paragraph-scrollbar">
<div class="p-24" style="padding-bottom: 8px; padding-top: 16px">
<div class="flex-between mb-16">
<div class="bold title align-center">
选择分段
<el-text> 已选分段{{ associationCount(currentDocument) }} </el-text>
</div>
<el-input
v-model="search"
placeholder="搜索"
class="input-with-select"
style="width: 260px"
@change="searchHandle"
>
<template #prepend>
<el-select v-model="searchType" placeholder="Select" style="width: 80px">
<el-option label="标题" value="title" />
<el-option label="内容" value="content" />
</el-select>
</template>
</el-input>
</div>
<el-empty v-if="paragraphList.length == 0" description="暂无数据" />
<InfiniteScroll
v-else
:size="paragraphList.length"
:total="paginationConfig.total"
:page_size="paginationConfig.page_size"
v-model:current_page="paginationConfig.current_page"
@load="getParagraphList"
:loading="loading"
>
<template v-for="(item, index) in paragraphList" :key="index">
<CardBox
shadow="hover"
:title="item.title || '-'"
:description="item.content"
class="paragraph-card cursor mb-16"
:class="isAssociation(item.id) ? 'active' : ''"
:showIcon="false"
@click="associationClick(item)"
>
</CardBox>
</template>
</InfiniteScroll>
</div>
</el-scrollbar>
</el-col>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { useRoute } from 'vue-router'
import problemApi from '@/api/problem'
import paragraphApi from '@/api/paragraph'
import useStore from '@/stores'
const { problem, document } = useStore()
const route = useRoute()
const {
params: { id } // datasetId
} = route as any
const emit = defineEmits(['refresh'])
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const documentList = ref<any[]>([])
const paragraphList = ref<any[]>([])
const currentProblemId = ref<String>('')
//
const associationParagraph = ref<any[]>([])
const currentDocument = ref<String>('')
const search = ref('')
const searchType = ref('title')
const paginationConfig = reactive({
current_page: 1,
page_size: 50,
total: 0
})
function associationClick(item: any) {
if (isAssociation(item.id)) {
problem
.asyncDisassociationProblem(
id,
item.document_id,
item.id,
currentProblemId.value as string,
loading
)
.then(() => {
getRecord(currentProblemId.value)
})
} else {
problem
.asyncAssociationProblem(
id,
item.document_id,
item.id,
currentProblemId.value as string,
loading
)
.then(() => {
getRecord(currentProblemId.value)
})
}
}
function searchHandle() {
paginationConfig.current_page = 1
paragraphList.value = []
getParagraphList(currentDocument.value)
}
function clickDocumentHandle(item: any) {
paginationConfig.current_page = 1
paragraphList.value = []
currentDocument.value = item.id
getParagraphList(item.id)
}
function getDocument() {
document.asyncGetAllDocument(id, loading).then((res: any) => {
documentList.value = res.data
currentDocument.value = documentList.value?.length > 0 ? documentList.value[0].id : ''
getParagraphList(currentDocument.value)
})
}
function getParagraphList(documentId: String) {
paragraphApi
.getParagraph(
id,
(documentId || currentDocument.value) as string,
paginationConfig,
search.value && { [searchType.value]: search.value },
loading
)
.then((res) => {
paragraphList.value = [...paragraphList.value, ...res.data.records]
paginationConfig.total = res.data.total
})
}
//
function getRecord(problemId: String) {
problemApi.getDetailProblems(id as string, problemId as string, loading).then((res) => {
associationParagraph.value = res.data
})
}
function associationCount(documentId: String) {
return associationParagraph.value.filter((item) => item.document_id === documentId).length
}
function isAssociation(paragraphId: String) {
return associationParagraph.value.some((option) => option.id === paragraphId)
}
watch(dialogVisible, (bool) => {
if (!bool) {
documentList.value = []
paragraphList.value = []
associationParagraph.value = []
currentDocument.value = ''
search.value = ''
searchType.value = 'title'
emit('refresh')
}
})
const open = (problemId: string) => {
currentProblemId.value = problemId
getDocument()
getRecord(problemId)
dialogVisible.value = true
}
defineExpose({ open })
</script>
<style lang="scss" scope>
.paragraph-card {
position: relative;
&.active {
border: 1px solid var(--el-color-primary);
&:before {
content: '';
position: absolute;
right: 0;
top: 0;
border: 14px solid var(--el-color-primary);
border-bottom-color: transparent;
border-left-color: transparent;
}
&:after {
content: '';
width: 3px;
height: 6px;
position: absolute;
right: 5px;
top: 2px;
border: 2px solid #fff;
border-top-color: transparent;
border-left-color: transparent;
transform: rotate(35deg);
}
}
}
.paragraph-badge {
.el-badge__content {
height: auto;
display: table;
}
}
</style>

View File

@ -0,0 +1,340 @@
<template>
<LayoutContainer header="问题">
<div class="main-calc-height">
<div class="p-24">
<div class="flex-between">
<div>
<el-button type="primary" @click="createProblem">创建问题</el-button>
<el-button @click="deleteMulDocument" :disabled="multipleSelection.length === 0"
>批量删除</el-button
>
</div>
<el-input
v-model="filterText"
placeholder="搜索内容"
prefix-icon="Search"
class="w-240"
@change="getList"
/>
</div>
<app-table
ref="multipleTableRef"
class="mt-16"
:data="problemData"
:pagination-config="paginationConfig"
quick-create
quickCreateName="问题"
quickCreatePlaceholder="快速创建问题"
:quickCreateMaxlength="256"
@sizeChange="handleSizeChange"
@changePage="getList"
@cell-mouse-enter="cellMouseEnter"
@cell-mouse-leave="cellMouseLeave"
@creatQuick="creatQuickHandle"
@row-click="rowClickHandle"
@selection-change="handleSelectionChange"
:row-class-name="setRowClass"
v-loading="loading"
:row-key="(row: any) => row.id"
>
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="content" label="问题" min-width="280">
<template #default="{ row }">
<ReadWrite
@change="editName"
:data="row.content"
:showEditIcon="row.id === currentMouseId"
:maxlength="256"
/>
</template>
</el-table-column>
<el-table-column prop="paragraph_count" label="关联分段数" align="right" min-width="100">
<template #default="{ row }">
<el-link type="primary" @click.stop="rowClickHandle(row)" v-if="row.paragraph_count">
{{ row.paragraph_count }}
</el-link>
<span v-else>
{{ row.paragraph_count }}
</span>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="170">
<template #default="{ row }">
{{ datetimeFormat(row.create_time) }}
</template>
</el-table-column>
<el-table-column prop="update_time" label="更新时间" width="170">
<template #default="{ row }">
{{ datetimeFormat(row.update_time) }}
</template>
</el-table-column>
<el-table-column label="操作" align="left">
<template #default="{ row }">
<div>
<span class="mr-4">
<el-tooltip effect="dark" content="关联分段" placement="top">
<el-button type="primary" text @click.stop="relateProblem(row)">
<el-icon><Connection /></el-icon>
</el-button>
</el-tooltip>
</span>
<span>
<el-tooltip effect="dark" content="删除" placement="top">
<el-button type="primary" text @click.stop="deleteProblem(row)">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</span>
</div>
</template>
</el-table-column>
</app-table>
</div>
</div>
<CreateProblemDialog ref="CreateProblemDialogRef" @refresh="refresh" />
<DetailProblemDrawer
:next="nextChatRecord"
:pre="preChatRecord"
ref="DetailProblemRef"
v-model:currentId="currentClickId"
v-model:currentContent="currentContent"
:pre_disable="pre_disable"
:next_disable="next_disable"
@refresh="refresh"
/>
<RelateProblemDialog ref="RelateProblemDialogRef" @refresh="refresh" />
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, onBeforeUnmount, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElTable } from 'element-plus'
import problemApi from '@/api/problem'
import CreateProblemDialog from './component/CreateProblemDialog.vue'
import DetailProblemDrawer from './component/DetailProblemDrawer.vue'
import RelateProblemDialog from './component/RelateProblemDialog.vue'
import { datetimeFormat } from '@/utils/time'
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
import type { Dict } from '@/api/type/common'
import useStore from '@/stores'
const route = useRoute()
const {
params: { id }
} = route as any
const { problem } = useStore()
const RelateProblemDialogRef = ref()
const DetailProblemRef = ref()
const CreateProblemDialogRef = ref()
const loading = ref(false)
// id
const currentMouseId = ref('')
// drawerid
const currentClickId = ref('')
const currentContent = ref('')
const paginationConfig = reactive({
current_page: 1,
page_size: 10,
total: 0
})
const filterText = ref('')
const problemData = ref<any[]>([])
const problemIndexMap = computed<Dict<number>>(() => {
return problemData.value
.map((row, index) => ({
[row.id]: index
}))
.reduce((pre, next) => ({ ...pre, ...next }), {})
})
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
const multipleSelection = ref<any[]>([])
function relateProblem(row: any) {
RelateProblemDialogRef.value.open(row.id)
}
function createProblem() {
CreateProblemDialogRef.value.open()
}
const handleSelectionChange = (val: any[]) => {
multipleSelection.value = val
}
/*
快速创建空白文档
*/
function creatQuickHandle(val: string) {
loading.value = true
const obj = [val]
problem
.asyncPostProblem(id, obj)
.then((res) => {
getList()
MsgSuccess('创建成功')
})
.catch(() => {
loading.value = false
})
}
function deleteMulDocument() {
const arr: string[] = []
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
problemApi.delMulProblem(id, arr, loading).then(() => {
MsgSuccess('批量删除成功')
getList()
})
}
function deleteProblem(row: any) {
MsgConfirm(
`是否删除问题:${row.content} ?`,
`删除问题关联的 ${row.paragraph_count} 个分段会被取消关联,请谨慎操作。`,
{
confirmButtonText: '删除',
confirmButtonClass: 'danger'
}
)
.then(() => {
problemApi.delProblems(id, row.id, loading).then(() => {
MsgSuccess('删除成功')
getList()
})
})
.catch(() => {})
}
function editName(val: string) {
if (val) {
const obj = {
content: val
}
problemApi.putProblems(id, currentMouseId.value, obj, loading).then(() => {
getList()
MsgSuccess('修改成功')
})
} else {
MsgError('问题不能为空!')
}
}
function cellMouseEnter(row: any) {
currentMouseId.value = row.id
}
function cellMouseLeave() {
currentMouseId.value = ''
}
/**
* 下一页
*/
const nextChatRecord = () => {
let index = problemIndexMap.value[currentClickId.value] + 1
if (index >= problemData.value.length) {
if (
index + (paginationConfig.current_page - 1) * paginationConfig.page_size >=
paginationConfig.total - 1
) {
return
}
paginationConfig.current_page = paginationConfig.current_page + 1
getList().then(() => {
index = 0
currentClickId.value = problemData.value[index].id
currentContent.value = problemData.value[index].content
})
} else {
currentClickId.value = problemData.value[index].id
currentContent.value = problemData.value[index].content
}
}
const pre_disable = computed(() => {
let index = problemIndexMap.value[currentClickId.value] - 1
return index < 0 && paginationConfig.current_page <= 1
})
const next_disable = computed(() => {
let index = problemIndexMap.value[currentClickId.value] + 1
return (
index >= problemData.value.length &&
index + (paginationConfig.current_page - 1) * paginationConfig.page_size >=
paginationConfig.total - 1
)
})
/**
* 上一页
*/
const preChatRecord = () => {
let index = problemIndexMap.value[currentClickId.value] - 1
if (index < 0) {
if (paginationConfig.current_page <= 1) {
return
}
paginationConfig.current_page = paginationConfig.current_page - 1
getList().then((ok) => {
index = paginationConfig.page_size - 1
currentClickId.value = problemData.value[index].id
currentContent.value = problemData.value[index].content
})
} else {
currentClickId.value = problemData.value[index].id
currentContent.value = problemData.value[index].content
}
}
function rowClickHandle(row: any) {
if (row.paragraph_count) {
currentClickId.value = row.id
currentContent.value = row.content
DetailProblemRef.value.open()
}
}
const setRowClass = ({ row }: any) => {
return currentClickId.value === row?.id ? 'hightlight' : ''
}
function handleSizeChange() {
paginationConfig.current_page = 1
getList()
}
function getList() {
return problem
.asyncGetProblem(
id as string,
paginationConfig,
filterText.value && { content: filterText.value },
loading
)
.then((res: any) => {
problemData.value = res.data.records
paginationConfig.total = res.data.total
})
}
function refresh() {
paginationConfig.current_page = 1
getList()
}
onMounted(() => {
getList()
})
onBeforeUnmount(() => {})
</script>
<style lang="scss" scoped></style>