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-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@vavt/cm-extension": "^1.9.1",
"@wecom/jssdk": "^2.3.1",
"axios": "^1.8.4",
"cropperjs": "^2.0.0-rc.2",
"dingtalk-jsapi": "^3.1.0",
"element-plus": "^2.9.10",
"highlight.js": "^11.11.1",
"katex": "^0.16.22",
"md-editor-v3": "^5.6.1",
"mermaid": "^11.6.0",
"nprogress": "^0.2.0",
"pinia": "^3.0.1",
"screenfull": "^6.0.2",
"use-element-plus-theme": "^0.0.5",
"vue": "^3.5.13",
"vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "^11.1.3",
"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 MdPreview from './markdown/MdPreview.vue'
import MdEditorMagnify from './markdown/MdEditorMagnify.vue'
import TagEllipsis from './tag-ellipsis/index.vue'
export default {
install(app: App) {
app.component('LogoFull', LogoFull)
@ -41,5 +42,6 @@ export default {
app.component('MdPreview', MdPreview)
app.component('MdEditor', MdEditor)
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 Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import mermaid from 'mermaid'

View File

@ -7,6 +7,7 @@ import useKnowledgeStore from './modules/knowledge'
import useModelStore from './modules/model'
import usePromptStore from './modules/prompt'
import useProblemStore from './modules/problem'
import useParagraphStore from './modules/paragraph'
const useStore = () => ({
common: useCommonStore(),
@ -18,6 +19,7 @@ const useStore = () => ({
model: useModelStore(),
prompt: usePromptStore(),
problem: useProblemStore(),
paragraph: useParagraphStore(),
})
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;
}
}
// 分段 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 './app.scss';
@use './component.scss';
@use './md-editor.scss';
@import 'nprogress/nprogress.css';
@import 'md-editor-v3/lib/style.css';
@import './md-editor.scss';

View File

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

View File

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

View File

@ -25,7 +25,7 @@
</div>
<el-card
style="--el-card-padding: 0"
class="paragraph-detail__main mt-16"
class="paragraph__main mt-16"
v-loading="(paginationConfig.current_page === 1 && loading) || changeStateloading"
>
<div class="flex-between p-12-16 border-b">
@ -46,22 +46,58 @@
</template>
</el-input>
</div>
<el-empty v-if="paragraphDetail.length == 0" :description="$t('common.noData')" />
<div v-else>
<el-scrollbar>
<div class="paragraph-detail-height">
<InfiniteScroll
:size="paragraphDetail.length"
:total="paginationConfig.total"
:page_size="paginationConfig.page_size"
v-model:current_page="paginationConfig.current_page"
@load="getParagraphList"
:loading="loading"
>
</InfiniteScroll>
<div class="flex">
<div class="paragraph-sidebar p-16 border-r">
<el-anchor
direction="vertical"
type="default"
:offset="130"
container=".paragraph-scollbar"
@click="handleClick"
>
<template v-for="(item, index) in paragraphDetail" :key="item.id">
<el-anchor-link :href="`#${item.id}`" :title="item.title" v-if="item.title" />
</template>
</el-anchor>
</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>
</el-scrollbar>
</div>
</div>
<div class="mul-operation border-t w-full" v-if="isBatch === true">
@ -89,12 +125,15 @@
<script setup lang="ts">
import { reactive, ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { cloneDeep } from 'lodash'
import documentApi from '@/api/knowledge/document'
import paragraphApi from '@/api/knowledge/paragraph'
import ParagraphDialog from './component/ParagraphDialog.vue'
import ParagraphCard from './component/ParagraphCard.vue'
import SelectDocumentDialog from './component/SelectDocumentDialog.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 useStore from '@/stores'
import { t } from '@/locales'
@ -104,6 +143,7 @@ const {
params: { id, documentId },
} = route as any
const containerRef = ref<HTMLElement | null>(null)
const SelectDocumentDialogRef = ref()
const ParagraphDialogRef = ref()
const loading = ref(false)
@ -114,6 +154,10 @@ const title = ref('')
const search = ref('')
const searchType = ref('title')
const handleClick = (e: MouseEvent) => {
e.preventDefault()
}
//
const isBatch = ref(false)
const multipleSelection = ref<any[]>([])
@ -279,6 +323,20 @@ function openGenerateDialog(row?: any) {
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(() => {
getDetail()
getParagraphList()
@ -292,43 +350,14 @@ onMounted(() => {
right: calc(var(--app-base-px) * 3);
top: calc(var(--app-base-px) + 4px);
}
.paragraph-detail-height {
height: calc(var(--app-main-height) - 75px);
.paragraph-sidebar {
width: 240px;
}
.paragraph-card {
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);
}
&.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;
}
.paragraph-detail {
height: calc(100vh - 215px);
max-width: 1000px;
margin: 16px auto;
}
&__main {
@ -343,5 +372,17 @@ onMounted(() => {
background: #ffffff;
}
}
.paragraph-card {
&.handle {
.handle-img {
visibility: hidden;
}
&:hover {
.handle-img {
visibility: visible;
}
}
}
}
}
</style>