feat: paragraph

This commit is contained in:
wangdan-fit2cloud 2025-06-06 20:08:15 +08:00
parent 1390ddb36e
commit 003b9115a3
12 changed files with 235 additions and 122 deletions

View File

@ -16,17 +16,24 @@
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@vavt/cm-extension": "^1.9.1",
"@wecom/jssdk": "^2.3.1", "@wecom/jssdk": "^2.3.1",
"axios": "^1.8.4", "axios": "^1.8.4",
"cropperjs": "^2.0.0-rc.2",
"dingtalk-jsapi": "^3.1.0", "dingtalk-jsapi": "^3.1.0",
"element-plus": "^2.9.10", "element-plus": "^2.9.10",
"highlight.js": "^11.11.1",
"katex": "^0.16.22",
"md-editor-v3": "^5.6.1", "md-editor-v3": "^5.6.1",
"mermaid": "^11.6.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"screenfull": "^6.0.2",
"use-element-plus-theme": "^0.0.5", "use-element-plus-theme": "^0.0.5",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1", "vue-codemirror": "^6.1.1",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "^11.1.3", "vue-i18n": "^11.1.3",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },

1
ui/src/assets/sort.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744092984968" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1250" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M384 768a64 64 0 1 0 0 128 64 64 0 0 0 0-128z m0-320a64 64 0 1 0 0 128 64 64 0 0 0 0-128z m0-320a64 64 0 1 0 0 128 64 64 0 0 0 0-128z m256 640a64 64 0 1 0 0 128 64 64 0 0 0 0-128z m0-320a64 64 0 1 0 0 128 64 64 0 0 0 0-128z m0-320a64 64 0 1 0 0 128 64 64 0 0 0 0-128z" p-id="1251"></path></svg>

After

Width:  |  Height:  |  Size: 627 B

View File

@ -19,6 +19,7 @@ import AutoTooltip from './auto-tooltip/index.vue'
import MdEditor from './markdown/MdEditor.vue' import MdEditor from './markdown/MdEditor.vue'
import MdPreview from './markdown/MdPreview.vue' import MdPreview from './markdown/MdPreview.vue'
import MdEditorMagnify from './markdown/MdEditorMagnify.vue' import MdEditorMagnify from './markdown/MdEditorMagnify.vue'
import TagEllipsis from './tag-ellipsis/index.vue'
export default { export default {
install(app: App) { install(app: App) {
app.component('LogoFull', LogoFull) app.component('LogoFull', LogoFull)
@ -41,5 +42,6 @@ export default {
app.component('MdPreview', MdPreview) app.component('MdPreview', MdPreview)
app.component('MdEditor', MdEditor) app.component('MdEditor', MdEditor)
app.component('MdEditorMagnify', MdEditorMagnify) app.component('MdEditorMagnify', MdEditorMagnify)
app.component('TagEllipsis', TagEllipsis)
}, },
} }

View File

@ -0,0 +1,26 @@
<template>
<el-tag class="tag-ellipsis flex-between mb-8" effect="plain" v-bind="$attrs">
<slot></slot>
</el-tag>
</template>
<script setup lang="ts">
defineOptions({ name: 'TagEllipsis' })
</script>
<style lang="scss" scoped>
/* tag超出省略号 */
.tag-ellipsis {
border: 1px solid var(--el-border-color);
color: var(--app-text-color);
border-radius: 4px;
height: 30px;
line-height: 30px;
padding: 0 9px;
box-sizing: border-box;
:deep(.el-tag__content) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>

View File

@ -19,7 +19,6 @@ import katex from 'katex'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import Cropper from 'cropperjs' import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import mermaid from 'mermaid' import mermaid from 'mermaid'

View File

@ -7,6 +7,7 @@ import useKnowledgeStore from './modules/knowledge'
import useModelStore from './modules/model' import useModelStore from './modules/model'
import usePromptStore from './modules/prompt' import usePromptStore from './modules/prompt'
import useProblemStore from './modules/problem' import useProblemStore from './modules/problem'
import useParagraphStore from './modules/paragraph'
const useStore = () => ({ const useStore = () => ({
common: useCommonStore(), common: useCommonStore(),
@ -18,6 +19,7 @@ const useStore = () => ({
model: useModelStore(), model: useModelStore(),
prompt: usePromptStore(), prompt: usePromptStore(),
problem: useProblemStore(), problem: useProblemStore(),
paragraph: useParagraphStore(),
}) })
export default useStore export default useStore

View File

@ -0,0 +1,47 @@
import { defineStore } from 'pinia'
import paragraphApi from '@/api/knowledge/paragraph'
import type { Ref } from 'vue'
const useParagraphStore = defineStore('paragraph', {
state: () => ({}),
actions: {
async asyncPutParagraph(
datasetId: string,
documentId: string,
paragraphId: string,
data: any,
loading?: Ref<boolean>,
) {
return new Promise((resolve, reject) => {
paragraphApi
.putParagraph(datasetId, documentId, paragraphId, data, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncDelParagraph(
datasetId: string,
documentId: string,
paragraphId: string,
loading?: Ref<boolean>,
) {
return new Promise((resolve, reject) => {
paragraphApi
.delParagraph(datasetId, documentId, paragraphId, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
},
})
export default useParagraphStore

View File

@ -99,3 +99,25 @@
padding: 0 !important; padding: 0 !important;
} }
} }
// 分段 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

@ -3,6 +3,7 @@
@use './variables.scss'; @use './variables.scss';
@use './app.scss'; @use './app.scss';
@use './component.scss'; @use './component.scss';
@use './md-editor.scss';
@import 'nprogress/nprogress.css'; @import 'nprogress/nprogress.css';
@import 'md-editor-v3/lib/style.css'; @import 'md-editor-v3/lib/style.css';
@import './md-editor.scss';

View File

@ -59,7 +59,7 @@
<AppIcon <AppIcon
:iconName="'app-magnify'" :iconName="'app-magnify'"
:style="{ :style="{
color: xpackForm.custom_theme?.header_font_color color: xpackForm.custom_theme?.header_font_color,
}" }"
style="font-size: 20px" style="font-size: 20px"
></AppIcon> ></AppIcon>
@ -69,7 +69,7 @@
:size="20" :size="20"
class="color-secondary" class="color-secondary"
:style="{ :style="{
color: xpackForm.custom_theme?.header_font_color color: xpackForm.custom_theme?.header_font_color,
}" }"
> >
<Close/> <Close/>
@ -305,7 +305,7 @@
<el-option <el-option
:label=" :label="
$t( $t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.left' 'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.left',
) )
" "
value="left" value="left"
@ -313,7 +313,7 @@
<el-option <el-option
:label=" :label="
$t( $t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.right' 'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.right',
) )
" "
value="right" value="right"
@ -337,7 +337,7 @@
<el-option <el-option
:label=" :label="
$t( $t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.top' 'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.top',
) )
" "
value="top" value="top"
@ -345,7 +345,7 @@
<el-option <el-option
:label=" :label="
$t( $t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.bottom' 'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.bottom',
) )
" "
value="bottom" value="bottom"
@ -453,14 +453,14 @@ const defaultSetting = {
disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'), disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'),
custom_theme: { custom_theme: {
theme_color: '', theme_color: '',
header_font_color: '#1f2329' header_font_color: '#1f2329',
}, },
float_location: { float_location: {
y: {type: 'bottom', value: 30}, y: {type: 'bottom', value: 30},
x: {type: 'right', value: 0} x: {type: 'right', value: 0}
}, },
show_avatar: true, show_avatar: true,
show_user_avatar: false show_user_avatar: false,
} }
const displayFormRef = ref() const displayFormRef = ref()
@ -486,14 +486,14 @@ const xpackForm = ref<any>({
disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'), disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'),
custom_theme: { custom_theme: {
theme_color: '', theme_color: '',
header_font_color: '#1f2329' header_font_color: '#1f2329',
}, },
float_location: { float_location: {
y: {type: 'bottom', value: 30}, y: {type: 'bottom', value: 30},
x: {type: 'right', value: 0} x: {type: 'right', value: 0}
}, },
show_avatar: true, show_avatar: true,
show_user_avatar: false show_user_avatar: false,
}) })
const imgUrl = ref<any>({ const imgUrl = ref<any>({
@ -512,7 +512,7 @@ const detail = ref<any>(null)
const customStyle = computed(() => { const customStyle = computed(() => {
return { return {
background: xpackForm.value.custom_theme?.theme_color, background: xpackForm.value.custom_theme?.theme_color,
color: xpackForm.value.custom_theme?.header_font_color color: xpackForm.value.custom_theme?.header_font_color,
} }
}) })
@ -561,7 +561,7 @@ const open = (data: any, content: any) => {
t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue') t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue')
) { ) {
xpackForm.value.disclaimer_value = t( xpackForm.value.disclaimer_value = t(
'views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue' 'views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue',
) )
} }
xpackForm.value.avatar_url = data.avatar xpackForm.value.avatar_url = data.avatar
@ -573,7 +573,7 @@ const open = (data: any, content: any) => {
xpackForm.value.show_user_avatar = data.show_user_avatar xpackForm.value.show_user_avatar = data.show_user_avatar
xpackForm.value.custom_theme = { xpackForm.value.custom_theme = {
theme_color: data.custom_theme?.theme_color || '', theme_color: data.custom_theme?.theme_color || '',
header_font_color: data.custom_theme?.header_font_color || '#1f2329' header_font_color: data.custom_theme?.header_font_color || '#1f2329',
} }
xpackForm.value.float_location = data.float_location xpackForm.value.float_location = data.float_location
dialogVisible.value = true dialogVisible.value = true
@ -613,7 +613,7 @@ defineExpose({open})
</script> </script>
<style lang="scss"> <style lang="scss">
.setting-preview { .setting-preview {
background: #f5f6f7; background: var(--app-layout-bg-color);
height: 570px; height: 570px;
position: relative; position: relative;

View File

@ -1,12 +1,15 @@
<template> <template>
<el-card shadow="hover" class="paragraph-box" @mouseenter="cardEnter()" @mouseleave="cardLeave()"> <el-card
<div class="card-header"> shadow="hover"
<h2>{{ 1111 }}</h2> class="paragraph-box cursor"
</div> @mouseenter="cardEnter()"
@mouseleave="cardLeave()"
>
<h2 class="mb-16">{{ data.title || '-' }}</h2>
<MdPreview <MdPreview
ref="editorRef" ref="editorRef"
editorId="preview-only" editorId="preview-only"
:modelValue="form.content" :modelValue="data.content"
class="maxkb-md" class="maxkb-md"
/> />
</el-card> </el-card>
@ -14,24 +17,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, useSlots } from 'vue' import { ref, useSlots } from 'vue'
import { t } from '@/locales' import { t } from '@/locales'
defineOptions({ name: 'CardBox' }) const props = defineProps<{
const props = withDefaults( data: any
defineProps<{ }>()
/**
* 标题
*/
title?: string
/**
* 描述
*/
description?: string
/**
* 是否展示icon
*/
showIcon?: boolean
}>(),
{ title: t('common.title'), description: '', showIcon: true, border: true },
)
const show = ref(false) const show = ref(false)
// carddropdown // carddropdown
@ -50,36 +38,13 @@ function subHoveredEnter() {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.card-box { .paragraph-box {
font-size: 14px; background: var(--app-layout-bg-color);
position: relative; border: 1px solid #ffffff;
min-height: var(--card-min-height); box-shadow: none !important;
min-width: var(--card-min-width); &:hover {
.card-header { background: rgba(31, 35, 41, 0.1);
margin-top: -5px; border: 1px solid #dee0e3;
}
.description {
line-height: 22px;
font-weight: 400;
min-height: 70px;
.content {
display: -webkit-box;
height: var(--app-card-box-description-height, 40px);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.card-footer {
position: absolute;
bottom: 8px;
left: 0;
min-height: 30px;
font-weight: 400;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
} }
} }
</style> </style>

View File

@ -25,7 +25,7 @@
</div> </div>
<el-card <el-card
style="--el-card-padding: 0" style="--el-card-padding: 0"
class="paragraph-detail__main mt-16" class="paragraph__main mt-16"
v-loading="(paginationConfig.current_page === 1 && loading) || changeStateloading" v-loading="(paginationConfig.current_page === 1 && loading) || changeStateloading"
> >
<div class="flex-between p-12-16 border-b"> <div class="flex-between p-12-16 border-b">
@ -46,22 +46,58 @@
</template> </template>
</el-input> </el-input>
</div> </div>
<el-empty v-if="paragraphDetail.length == 0" :description="$t('common.noData')" /> <div class="flex">
<div v-else> <div class="paragraph-sidebar p-16 border-r">
<el-scrollbar> <el-anchor
<div class="paragraph-detail-height"> direction="vertical"
<InfiniteScroll type="default"
:size="paragraphDetail.length" :offset="130"
:total="paginationConfig.total" container=".paragraph-scollbar"
:page_size="paginationConfig.page_size" @click="handleClick"
v-model:current_page="paginationConfig.current_page" >
@load="getParagraphList" <template v-for="(item, index) in paragraphDetail" :key="item.id">
:loading="loading" <el-anchor-link :href="`#${item.id}`" :title="item.title" v-if="item.title" />
> </template>
</el-anchor>
</InfiniteScroll> </div>
<div class="w-full">
<el-empty v-if="paragraphDetail.length == 0" :description="$t('common.noData')" />
<div v-else>
<el-scrollbar class="paragraph-scollbar">
<div class="paragraph-detail">
<InfiniteScroll
:size="paragraphDetail.length"
:total="paginationConfig.total"
:page_size="paginationConfig.page_size"
v-model:current_page="paginationConfig.current_page"
@load="getParagraphList"
:loading="loading"
>
<VueDraggable
ref="el"
v-bind:modelValue="paragraphDetail"
handle=".handle"
:animation="150"
ghostClass="ghost"
@end="onEnd"
>
<template v-for="(item, index) in paragraphDetail" :key="item.id">
<div class="handle paragraph-card flex" :id="item.id">
<img
src="@/assets/sort.svg"
alt=""
height="15"
class="handle-img mr-8 mt-24 cursor"
/>
<ParagraphCard :data="item" class="mb-8 w-full" />
</div>
</template>
</VueDraggable>
</InfiniteScroll>
</div>
</el-scrollbar>
</div> </div>
</el-scrollbar> </div>
</div> </div>
<div class="mul-operation border-t w-full" v-if="isBatch === true"> <div class="mul-operation border-t w-full" v-if="isBatch === true">
@ -89,12 +125,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, onMounted, computed } from 'vue' import { reactive, ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { cloneDeep } from 'lodash'
import documentApi from '@/api/knowledge/document' import documentApi from '@/api/knowledge/document'
import paragraphApi from '@/api/knowledge/paragraph' import paragraphApi from '@/api/knowledge/paragraph'
import ParagraphDialog from './component/ParagraphDialog.vue' import ParagraphDialog from './component/ParagraphDialog.vue'
import ParagraphCard from './component/ParagraphCard.vue'
import SelectDocumentDialog from './component/SelectDocumentDialog.vue' import SelectDocumentDialog from './component/SelectDocumentDialog.vue'
import GenerateRelatedDialog from '@/components/generate-related-dialog/index.vue' import GenerateRelatedDialog from '@/components/generate-related-dialog/index.vue'
import { numberFormat } from '@/utils/utils' import { numberFormat } from '@/utils/common'
import { VueDraggable } from 'vue-draggable-plus'
import { MsgSuccess, MsgConfirm } from '@/utils/message' import { MsgSuccess, MsgConfirm } from '@/utils/message'
import useStore from '@/stores' import useStore from '@/stores'
import { t } from '@/locales' import { t } from '@/locales'
@ -104,6 +143,7 @@ const {
params: { id, documentId }, params: { id, documentId },
} = route as any } = route as any
const containerRef = ref<HTMLElement | null>(null)
const SelectDocumentDialogRef = ref() const SelectDocumentDialogRef = ref()
const ParagraphDialogRef = ref() const ParagraphDialogRef = ref()
const loading = ref(false) const loading = ref(false)
@ -114,6 +154,10 @@ const title = ref('')
const search = ref('') const search = ref('')
const searchType = ref('title') const searchType = ref('title')
const handleClick = (e: MouseEvent) => {
e.preventDefault()
}
// //
const isBatch = ref(false) const isBatch = ref(false)
const multipleSelection = ref<any[]>([]) const multipleSelection = ref<any[]>([])
@ -279,6 +323,20 @@ function openGenerateDialog(row?: any) {
GenerateRelatedDialogRef.value.open(arr, 'paragraph') GenerateRelatedDialogRef.value.open(arr, 'paragraph')
} }
function onEnd(event?: any) {
const { oldIndex, newIndex } = event
if (oldIndex === undefined || newIndex === undefined) return
const list = cloneDeep(paragraphDetail.value)
if (oldIndex === list.length - 1 || newIndex === list.length - 1) {
return
}
const newInstance = { ...list[oldIndex], type: list[newIndex].type, id: list[newIndex].id }
const oldInstance = { ...list[newIndex], type: list[oldIndex].type, id: list[oldIndex].id }
list[newIndex] = newInstance
list[oldIndex] = oldInstance
paragraphDetail.value = list
}
onMounted(() => { onMounted(() => {
getDetail() getDetail()
getParagraphList() getParagraphList()
@ -292,43 +350,14 @@ onMounted(() => {
right: calc(var(--app-base-px) * 3); right: calc(var(--app-base-px) * 3);
top: calc(var(--app-base-px) + 4px); top: calc(var(--app-base-px) + 4px);
} }
.paragraph-sidebar {
.paragraph-detail-height { width: 240px;
height: calc(var(--app-main-height) - 75px);
} }
.paragraph-card {
height: 210px; .paragraph-detail {
background: var(--app-layout-bg-color); height: calc(100vh - 215px);
border: 1px solid var(--app-layout-bg-color); max-width: 1000px;
&.selected { margin: 16px auto;
background: #ffffff;
&:hover {
background: #ffffff;
}
}
&:hover {
background: #ffffff;
border: 1px solid var(--el-border-color);
}
&.disabled {
background: var(--app-layout-bg-color);
border: 1px solid var(--app-layout-bg-color);
:deep(.description) {
color: var(--app-border-color-dark);
}
:deep(.title) {
color: var(--app-border-color-dark);
}
}
:deep(.content) {
-webkit-line-clamp: 5 !important;
height: 110px !important;
}
.active-button {
position: absolute;
right: 16px;
top: 16px;
}
} }
&__main { &__main {
@ -343,5 +372,17 @@ onMounted(() => {
background: #ffffff; background: #ffffff;
} }
} }
.paragraph-card {
&.handle {
.handle-img {
visibility: hidden;
}
&:hover {
.handle-img {
visibility: visible;
}
}
}
}
} }
</style> </style>