feat: 分段管理支持批量迁移,删除分段 #113,#103

This commit is contained in:
shaohuzhang1 2024-05-08 10:40:15 +08:00 committed by GitHub
parent 8204d5ff44
commit d4e742f7c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 645 additions and 55 deletions

View File

@ -51,8 +51,15 @@ class UpdateProblemArgs:
class UpdateEmbeddingDatasetIdArgs:
def __init__(self, source_id_list: List[str], target_dataset_id: str):
self.source_id_list = source_id_list
def __init__(self, paragraph_id_list: List[str], target_dataset_id: str):
self.paragraph_id_list = paragraph_id_list
self.target_dataset_id = target_dataset_id
class UpdateEmbeddingDocumentIdArgs:
def __init__(self, paragraph_id_list: List[str], target_document_id: str, target_dataset_id: str):
self.paragraph_id_list = paragraph_id_list
self.target_document_id = target_document_id
self.target_dataset_id = target_dataset_id
@ -213,13 +220,23 @@ class ListenerManagement:
@staticmethod
def update_embedding_dataset_id(args: UpdateEmbeddingDatasetIdArgs):
VectorStore.get_embedding_vector().update_by_source_ids(args.source_id_list,
{'dataset_id': args.target_dataset_id})
VectorStore.get_embedding_vector().update_by_paragraph_ids(args.paragraph_id_list,
{'dataset_id': args.target_dataset_id})
@staticmethod
def update_embedding_document_id(args: UpdateEmbeddingDocumentIdArgs):
VectorStore.get_embedding_vector().update_by_paragraph_ids(args.paragraph_id_list,
{'document_id': args.target_document_id,
'dataset_id': args.target_dataset_id})
@staticmethod
def delete_embedding_by_source_ids(source_ids: List[str]):
VectorStore.get_embedding_vector().delete_by_source_ids(source_ids, SourceType.PROBLEM)
@staticmethod
def delete_embedding_by_paragraph_ids(paragraph_ids: List[str]):
VectorStore.get_embedding_vector().delete_by_paragraph_ids(paragraph_ids)
@staticmethod
def delete_embedding_by_dataset_id_list(source_ids: List[str]):
VectorStore.get_embedding_vector().delete_by_dataset_id_list(source_ids)

View File

@ -166,10 +166,12 @@ class DocumentSerializers(ApiMixin, serializers.Serializer):
meta={})
else:
document_list.update(dataset_id=target_dataset_id)
paragraph_list.update(dataset_id=target_dataset_id)
# 修改向量信息
ListenerManagement.update_embedding_dataset_id(UpdateEmbeddingDatasetIdArgs(
[problem_paragraph_mapping.id for problem_paragraph_mapping in problem_paragraph_mapping_list],
[paragraph.id for paragraph in paragraph_list],
target_dataset_id))
# 修改段落信息
paragraph_list.update(dataset_id=target_dataset_id)
@staticmethod
def get_target_dataset_problem(target_dataset_id: str,

View File

@ -15,13 +15,13 @@ from drf_yasg import openapi
from rest_framework import serializers
from common.db.search import page_search
from common.event.listener_manage import ListenerManagement
from common.event.listener_manage import ListenerManagement, UpdateEmbeddingDocumentIdArgs, UpdateEmbeddingDatasetIdArgs
from common.exception.app_exception import AppApiException
from common.mixins.api_mixin import ApiMixin
from common.util.common import post
from common.util.field_message import ErrMessage
from dataset.models import Paragraph, Problem, Document, ProblemParagraphMapping
from dataset.serializers.common_serializers import update_document_char_length
from dataset.serializers.common_serializers import update_document_char_length, BatchSerializer
from dataset.serializers.problem_serializers import ProblemInstanceSerializer, ProblemSerializer, ProblemSerializers
from embedding.models import SourceType
@ -272,6 +272,167 @@ class ParagraphSerializers(ApiMixin, serializers.Serializer):
description='问题id')
]
class Batch(serializers.Serializer):
dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("知识库id"))
document_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("文档id"))
@transaction.atomic
def batch_delete(self, instance: Dict, with_valid=True):
if with_valid:
BatchSerializer(data=instance).is_valid(model=Paragraph, raise_exception=True)
self.is_valid(raise_exception=True)
paragraph_id_list = instance.get("id_list")
QuerySet(Paragraph).filter(id__in=paragraph_id_list).delete()
QuerySet(ProblemParagraphMapping).filter(paragraph_id__in=paragraph_id_list).delete()
# 删除向量库
ListenerManagement.delete_embedding_by_paragraph_ids(paragraph_id_list)
return True
class Migrate(ApiMixin, serializers.Serializer):
dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("知识库id"))
document_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("文档id"))
target_dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("目标知识库id"))
target_document_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("目标文档id"))
paragraph_id_list = serializers.ListField(required=True, error_messages=ErrMessage.char("段落列表"),
child=serializers.UUIDField(required=True,
error_messages=ErrMessage.uuid("段落id")))
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
document_list = QuerySet(Document).filter(
id__in=[self.data.get('document_id'), self.data.get('target_document_id')])
document_id = self.data.get('document_id')
target_document_id = self.data.get('target_document_id')
if document_id == target_document_id:
raise AppApiException(5000, "需要迁移的文档和目标文档一致")
if len([document for document in document_list if str(document.id) == self.data.get('document_id')]) < 1:
raise AppApiException(5000, f"文档id不存在【{self.data.get('document_id')}")
if len([document for document in document_list if
str(document.id) == self.data.get('target_document_id')]) < 1:
raise AppApiException(5000, f"目标文档id不存在【{self.data.get('target_document_id')}")
@transaction.atomic
def migrate(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
dataset_id = self.data.get('dataset_id')
target_dataset_id = self.data.get('target_dataset_id')
document_id = self.data.get('document_id')
target_document_id = self.data.get('target_document_id')
paragraph_id_list = self.data.get('paragraph_id_list')
paragraph_list = QuerySet(Paragraph).filter(dataset_id=dataset_id, document_id=document_id,
id__in=paragraph_id_list)
problem_paragraph_mapping_list = QuerySet(ProblemParagraphMapping).filter(paragraph__in=paragraph_list)
# 同数据集迁移
if target_dataset_id == dataset_id:
if len(problem_paragraph_mapping_list):
problem_paragraph_mapping_list = [
self.update_problem_paragraph_mapping(target_document_id,
problem_paragraph_mapping) for problem_paragraph_mapping
in
problem_paragraph_mapping_list]
# 修改mapping
QuerySet(ProblemParagraphMapping).bulk_update(problem_paragraph_mapping_list,
['document_id'])
# 修改向量段落信息
ListenerManagement.update_embedding_document_id(UpdateEmbeddingDocumentIdArgs(
[paragraph.id for paragraph in paragraph_list],
target_document_id, target_dataset_id))
# 修改段落信息
paragraph_list.update(document_id=target_document_id)
# 不同数据集迁移
else:
problem_list = QuerySet(Problem).filter(
id__in=[problem_paragraph_mapping.problem_id for problem_paragraph_mapping in
problem_paragraph_mapping_list])
# 目标数据集问题
target_problem_list = list(
QuerySet(Problem).filter(content__in=[problem.content for problem in problem_list],
dataset_id=target_dataset_id))
target_handle_problem_list = [
self.get_target_dataset_problem(target_dataset_id, target_document_id, problem_paragraph_mapping,
problem_list, target_problem_list) for
problem_paragraph_mapping
in
problem_paragraph_mapping_list]
create_problem_list = [problem for problem, is_create in target_handle_problem_list if
is_create is not None and is_create]
# 插入问题
QuerySet(Problem).bulk_create(create_problem_list)
# 修改mapping
QuerySet(ProblemParagraphMapping).bulk_update(problem_paragraph_mapping_list,
['problem_id', 'dataset_id', 'document_id'])
# 修改向量段落信息
ListenerManagement.update_embedding_document_id(UpdateEmbeddingDocumentIdArgs(
[paragraph.id for paragraph in paragraph_list],
target_document_id, target_dataset_id))
# 修改段落信息
paragraph_list.update(dataset_id=target_dataset_id, document_id=target_document_id)
@staticmethod
def update_problem_paragraph_mapping(target_document_id: str, problem_paragraph_mapping):
problem_paragraph_mapping.document_id = target_document_id
return problem_paragraph_mapping
@staticmethod
def get_target_dataset_problem(target_dataset_id: str,
target_document_id: str,
problem_paragraph_mapping,
source_problem_list,
target_problem_list):
source_problem_list = [source_problem for source_problem in source_problem_list if
source_problem.id == problem_paragraph_mapping.problem_id]
problem_paragraph_mapping.dataset_id = target_dataset_id
problem_paragraph_mapping.document_id = target_document_id
if len(source_problem_list) > 0:
problem_content = source_problem_list[-1].content
problem_list = [problem for problem in target_problem_list if problem.content == problem_content]
if len(problem_list) > 0:
problem = problem_list[-1]
problem_paragraph_mapping.problem_id = problem.id
return problem, False
else:
problem = Problem(id=uuid.uuid1(), dataset_id=target_dataset_id, content=problem_content)
target_problem_list.append(problem)
problem_paragraph_mapping.problem_id = problem.id
return problem, True
return None
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='dataset_id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='文档id'),
openapi.Parameter(name='document_id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='文档id'),
openapi.Parameter(name='target_dataset_id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='目标知识库id'),
openapi.Parameter(name='target_document_id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='目标知识库id')
]
@staticmethod
def get_request_body_api():
return openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),
title='段落id列表',
description="段落id列表"
)
class Operate(ApiMixin, serializers.Serializer):
# 段落id
paragraph_id = serializers.UUIDField(required=True, error_messages=ErrMessage.char(

View File

@ -25,6 +25,10 @@ urlpatterns = [
path('dataset/<str:dataset_id>/document/migrate/<str:target_dataset_id>', views.Document.Migrate.as_view()),
path('dataset/<str:dataset_id>/document/<str:document_id>/refresh', views.Document.Refresh.as_view()),
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph', views.Paragraph.as_view()),
path(
'dataset/<str:dataset_id>/document/<str:document_id>/paragraph/migrate/dataset/<str:target_dataset_id>/document/<str:target_document_id>',
views.Paragraph.BatchMigrate.as_view()),
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph/_batch', views.Paragraph.Batch.as_view()),
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph/<int:current_page>/<int:page_size>',
views.Paragraph.Page.as_view(), name='paragraph_page'),
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph/<paragraph_id>',

View File

@ -12,9 +12,10 @@ from rest_framework.views import APIView
from rest_framework.views import Request
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import Permission, Group, Operate
from common.constants.permission_constants import Permission, Group, Operate, CompareConstants
from common.response import result
from common.util.common import query_params_to_single_dict
from dataset.serializers.common_serializers import BatchSerializer
from dataset.serializers.paragraph_serializers import ParagraphSerializers
@ -168,6 +169,50 @@ class Paragraph(APIView):
o.is_valid(raise_exception=True)
return result.success(o.delete())
class Batch(APIView):
authentication_classes = [TokenAuth]
@action(methods=['DELETE'], detail=False)
@swagger_auto_schema(operation_summary="批量删除段落",
operation_id="批量删除段落",
request_body=
BatchSerializer.get_request_body_api(),
manual_parameters=ParagraphSerializers.Create.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, document_id: str):
return result.success(ParagraphSerializers.Batch(
data={"dataset_id": dataset_id, 'document_id': document_id}).batch_delete(request.data))
class BatchMigrate(APIView):
authentication_classes = [TokenAuth]
@action(methods=['PUT'], detail=False)
@swagger_auto_schema(operation_summary="批量迁移段落",
operation_id="批量迁移段落",
manual_parameters=ParagraphSerializers.Migrate.get_request_params_api(),
request_body=ParagraphSerializers.Migrate.get_request_body_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')),
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('target_dataset_id')),
compare=CompareConstants.AND
)
def put(self, request: Request, dataset_id: str, target_dataset_id: str, document_id: str, target_document_id):
return result.success(
ParagraphSerializers.Migrate(
data={'dataset_id': dataset_id, 'target_dataset_id': target_dataset_id,
'document_id': document_id,
'target_document_id': target_document_id,
'paragraph_id_list': request.data}).migrate())
class Page(APIView):
authentication_classes = [TokenAuth]

View File

@ -113,7 +113,7 @@ class BaseVectorStore(ABC):
return result[0]
@abstractmethod
def query(self, query_text:str,query_embedding: List[float], dataset_id_list: list[str],
def query(self, query_text: str, query_embedding: List[float], dataset_id_list: list[str],
exclude_document_id_list: list[str],
exclude_paragraph_list: list[str], is_active: bool, top_n: int, similarity: float,
search_mode: SearchMode):
@ -130,6 +130,10 @@ class BaseVectorStore(ABC):
def update_by_paragraph_id(self, paragraph_id: str, instance: Dict):
pass
@abstractmethod
def update_by_paragraph_ids(self, paragraph_ids: str, instance: Dict):
pass
@abstractmethod
def update_by_source_id(self, source_id: str, instance: Dict):
pass
@ -173,3 +177,7 @@ class BaseVectorStore(ABC):
@abstractmethod
def delete_by_paragraph_id(self, paragraph_id: str):
pass
@abstractmethod
def delete_by_paragraph_ids(self, paragraph_ids: List[str]):
pass

View File

@ -119,6 +119,9 @@ class PGVector(BaseVectorStore):
def update_by_paragraph_id(self, paragraph_id: str, instance: Dict):
QuerySet(Embedding).filter(paragraph_id=paragraph_id).update(**instance)
def update_by_paragraph_ids(self, paragraph_id: str, instance: Dict):
QuerySet(Embedding).filter(paragraph_id__in=paragraph_id).update(**instance)
def delete_by_dataset_id(self, dataset_id: str):
QuerySet(Embedding).filter(dataset_id=dataset_id).delete()
@ -139,6 +142,9 @@ class PGVector(BaseVectorStore):
def delete_by_paragraph_id(self, paragraph_id: str):
QuerySet(Embedding).filter(paragraph_id=paragraph_id).delete()
def delete_by_paragraph_ids(self, paragraph_ids: List[str]):
QuerySet(Embedding).filter(paragraph_id__in=paragraph_ids).delete()
class ISearch(ABC):
@abstractmethod

View File

@ -48,6 +48,24 @@ const delParagraph: (
)
}
/**
*
* @param dataset_id, document_id
*/
const delMulParagraph: (
dataset_id: string,
document_id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (dataset_id, document_id, data, loading) => {
return del(
`${prefix}/${dataset_id}/document/${document_id}/paragraph/_batch`,
undefined,
{ id_list: data },
loading
)
}
/**
*
* @param
@ -104,6 +122,33 @@ const putParagraph: (
)
}
/**
*
* @param dataset_id,target_dataset_id,
*/
const putMigrateMulParagraph: (
dataset_id: string,
document_id: string,
target_dataset_id: string,
target_document_id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (
dataset_id,
document_id,
target_dataset_id,
target_document_id,
data,
loading
) => {
return put(
`${prefix}/${dataset_id}/document/${document_id}/paragraph/migrate/dataset/${target_dataset_id}/document/${target_document_id}`,
data,
undefined,
loading
)
}
/**
*
* @param dataset_iddocument_idparagraph_id
@ -189,5 +234,7 @@ export default {
getProblem,
postProblem,
disassociationProblem,
associationProblem
associationProblem,
delMulParagraph,
putMigrateMulParagraph
}

View File

@ -573,3 +573,33 @@ h4 {
color: var(--app-text-color);
}
}
// card 选中样式
.selected {
border: 1px solid var(--el-color-primary) !important;
&: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);
}
&:hover {
border: 1px solid var(--el-color-primary);
}
}

View File

@ -20,15 +20,15 @@
v-if="datasetDetail.type === '1'"
>同步文档</el-button
>
<el-button @click="openDatasetDialog()" :disabled="multipleSelection.length === 0"
>迁移</el-button
>
<el-button @click="openBatchEditDocument" :disabled="multipleSelection.length === 0"
>设置</el-button
>
<el-button @click="deleteMulDocument" :disabled="multipleSelection.length === 0"
>删除</el-button
>
<el-button @click="openDatasetDialog()" :disabled="multipleSelection.length === 0">
迁移
</el-button>
<el-button @click="openBatchEditDocument" :disabled="multipleSelection.length === 0">
设置
</el-button>
<el-button @click="deleteMulDocument" :disabled="multipleSelection.length === 0">
删除
</el-button>
</div>
<el-input
@ -327,9 +327,7 @@ const handleSelectionChange = (val: any[]) => {
function openBatchEditDocument() {
const arr: string[] = multipleSelection.value.map((v) => v.id)
if (batchEditDocumentDialogRef) {
batchEditDocumentDialogRef?.value?.open(arr)
}
batchEditDocumentDialogRef?.value?.open(arr)
}
/**

View File

@ -0,0 +1,162 @@
<template>
<el-dialog title="选择知识库/文档" v-model="dialogVisible" width="500">
<el-form
ref="formRef"
:model="form"
label-position="top"
require-asterisk-position="right"
:rules="rules"
@submit.prevent
>
<el-form-item label="选择知识库" prop="dataset_id">
<el-select
v-model="form.dataset_id"
filterable
placeholder="请选择知识库"
:loading="optionLoading"
@change="changeDataset"
>
<el-option v-for="item in datasetList" :key="item.id" :label="item.name" :value="item.id">
<span class="flex align-center">
<AppAvatar
v-if="!item.dataset_id && item.type === '1'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.dataset_id && item.type === '0'"
class="mr-12"
shape="square"
:size="24"
>
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
{{ item.name }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="保存至文档" prop="document_id">
<el-select
v-model="form.document_id"
filterable
placeholder="请选择文档"
:loading="optionLoading"
>
<el-option
v-for="item in documentList"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="submitForm(formRef)" :loading="loading"> 迁移 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import paragraphApi from '@/api/paragraph'
import useStore from '@/stores'
const { dataset, document } = useStore()
const route = useRoute()
const {
params: { id, documentId }
} = route as any
const emit = defineEmits(['refresh'])
const formRef = ref()
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const form = ref<any>({
dataset_id: '',
document_id: ''
})
const rules = reactive<FormRules>({
dataset_id: [{ required: true, message: '请选择知识库', trigger: 'change' }],
document_id: [{ required: true, message: '请选择文档', trigger: 'change' }]
})
const datasetList = ref<any[]>([])
const documentList = ref<any[]>([])
const optionLoading = ref(false)
const paragraphList = ref<string[]>([])
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
dataset_id: '',
document_id: ''
}
datasetList.value = []
documentList.value = []
paragraphList.value = []
formRef.value?.clearValidate()
}
})
function changeDataset(id: string) {
form.value.document_id = ''
getDocument(id)
}
function getDocument(id: string) {
document.asyncGetAllDocument(id, loading).then((res: any) => {
documentList.value = res.data?.filter((v: any) => v.id !== documentId)
})
}
function getDataset() {
dataset.asyncGetAllDataset(loading).then((res: any) => {
datasetList.value = res.data
})
}
const open = (list: any) => {
paragraphList.value = list
getDataset()
formRef.value?.clearValidate()
dialogVisible.value = true
}
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
paragraphApi
.putMigrateMulParagraph(
id,
documentId,
form.value.dataset_id,
form.value.document_id,
paragraphList.value,
loading
)
.then(() => {
emit('refresh')
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scope></style>

View File

@ -8,7 +8,20 @@
></el-text
>
<div class="document-detail__header">
<el-button @click="addParagraph" type="primary" :disabled="loading"> 添加分段 </el-button>
<el-button @click="batchSelectedHandle(true)" v-if="isBatch === false">
批量选择
</el-button>
<el-button @click="batchSelectedHandle(false)" v-if="isBatch === true">
取消选择
</el-button>
<el-button
@click="addParagraph"
type="primary"
:disabled="loading"
v-if="isBatch === false"
>
添加分段
</el-button>
</div>
</template>
<div
@ -57,7 +70,28 @@
:key="index"
class="p-8"
>
<!-- 批量操作card -->
<CardBox
v-if="isBatch === true"
shadow="hover"
:title="item.title || '-'"
:description="item.content"
class="document-card cursor"
:class="multipleSelection.includes(item.id) ? 'selected' : ''"
:showIcon="false"
@click="selectHandle(item.id)"
>
<div class="active-button" @click.stop></div>
<template #footer>
<div class="footer-content flex-between">
<span> {{ numberFormat(item?.content.length) || 0 }} 字符 </span>
</div>
</template>
</CardBox>
<!-- 非批量操作card -->
<CardBox
v-else
shadow="hover"
:title="item.title || '-'"
:description="item.content"
@ -77,11 +111,25 @@
<template #footer>
<div class="footer-content flex-between">
<span> {{ numberFormat(item?.content.length) || 0 }} 字符 </span>
<el-tooltip effect="dark" content="删除" placement="top">
<el-button text @click.stop="deleteParagraph(item)" class="delete-button">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
<span @click.stop>
<el-dropdown trigger="click">
<el-button text>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openSelectDocumentDialog(item)">
<AppIcon iconName="app-migrate"></AppIcon>
迁移</el-dropdown-item
>
<el-dropdown-item icon="Delete" @click.stop="deleteParagraph(item)"
>删除</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</template>
</CardBox>
@ -90,8 +138,20 @@
</InfiniteScroll>
</div>
</el-scrollbar>
<div class="mul-operation border-t w-full" v-if="isBatch === true">
<el-button :disabled="multipleSelection.length === 0" @click="openSelectDocumentDialog">
迁移
</el-button>
<el-button :disabled="multipleSelection.length === 0" @click="deleteMulParagraph">
删除
</el-button>
<span class="ml-8"> 已选 {{ multipleSelection.length }} </span>
</div>
</div>
<ParagraphDialog ref="ParagraphDialogRef" :title="title" @refresh="refresh" />
<SelectDocumentDialog ref="SelectDocumentDialogRef" @refresh="refreshMigrateParagraph" />
</LayoutContainer>
</template>
<script setup lang="ts">
@ -100,6 +160,7 @@ import { useRoute } from 'vue-router'
import documentApi from '@/api/document'
import paragraphApi from '@/api/paragraph'
import ParagraphDialog from './component/ParagraphDialog.vue'
import SelectDocumentDialog from './component/SelectDocumentDialog.vue'
import { numberFormat } from '@/utils/utils'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import useStore from '@/stores'
@ -109,6 +170,7 @@ const {
params: { id, documentId }
} = route as any
const SelectDocumentDialogRef = ref()
const ParagraphDialogRef = ref()
const loading = ref(false)
const changeStateloading = ref(false)
@ -118,12 +180,66 @@ const title = ref('')
const search = ref('')
const searchType = ref('title')
//
const isBatch = ref(false)
const multipleSelection = ref<any[]>([])
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
function refreshMigrateParagraph() {
paragraphDetail.value = paragraphDetail.value.filter(
(v) => !multipleSelection.value.includes(v.id)
)
multipleSelection.value = []
MsgSuccess('迁移删除成功')
}
function openSelectDocumentDialog(row?: any) {
if (row) {
multipleSelection.value = [row.id]
}
SelectDocumentDialogRef.value.open(multipleSelection.value)
}
function deleteMulParagraph() {
MsgConfirm(
`是否批量删除 ${multipleSelection.value.length} 个分段?`,
`删除后无法恢复,请谨慎操作。`,
{
confirmButtonText: '删除',
confirmButtonClass: 'danger'
}
)
.then(() => {
paragraphApi
.delMulParagraph(id, documentId, multipleSelection.value, changeStateloading)
.then(() => {
paragraphDetail.value = paragraphDetail.value.filter(
(v) => !multipleSelection.value.includes(v.id)
)
multipleSelection.value = []
MsgSuccess('批量删除成功')
})
})
.catch(() => {})
}
function batchSelectedHandle(bool: boolean) {
isBatch.value = bool
multipleSelection.value = []
}
function selectHandle(id: string) {
if (multipleSelection.value.includes(id)) {
multipleSelection.value.splice(multipleSelection.value.indexOf(id), 1)
} else {
multipleSelection.value.push(id)
}
}
function searchHandle() {
paginationConfig.current_page = 1
paragraphDetail.value = []
@ -219,6 +335,12 @@ onMounted(() => {
height: 210px;
background: var(--app-layout-bg-color);
border: 1px solid var(--app-layout-bg-color);
&.selected {
background: #ffffff;
&:hover {
background: #ffffff;
}
}
&:hover {
background: #ffffff;
border: 1px solid var(--el-border-color);
@ -243,5 +365,18 @@ onMounted(() => {
top: 16px;
}
}
&__main {
position: relative;
box-sizing: border-box;
.mul-operation {
position: absolute;
bottom: 0;
left: 0;
padding: 16px 24px;
box-sizing: border-box;
background: #ffffff;
}
}
}
</style>

View File

@ -80,7 +80,7 @@
:title="item.title || '-'"
:description="item.content"
class="paragraph-card cursor mb-16"
:class="isAssociation(item.id) ? 'active' : ''"
:class="isAssociation(item.id) ? 'selected' : ''"
:showIcon="false"
@click="associationClick(item)"
>
@ -244,31 +244,6 @@ defineExpose({ open })
<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 {