init
This commit is contained in:
462
src/views/knowledge/aiSlice/components/DocumentDrawer.vue
Normal file
462
src/views/knowledge/aiSlice/components/DocumentDrawer.vue
Normal file
@@ -0,0 +1,462 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:close-on-click-modal="true"
|
||||
size="50%"
|
||||
:destroy-on-close="true"
|
||||
direction="rtl"
|
||||
class="document-drawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex gap-3 items-center py-1">
|
||||
<h2 class="max-w-md text-lg font-semibold text-gray-800 truncate">{{ documentTitle }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="px-4 py-2 h-full">
|
||||
<!-- 选项卡 -->
|
||||
<el-tabs v-model="activeTab" class="h-full flex flex-col">
|
||||
<!-- 文档展示选项卡 -->
|
||||
<el-tab-pane label="文档展示" name="document" class="flex-1">
|
||||
<div class="h-full">
|
||||
<el-scrollbar height="calc(100vh - 180px)" ref="scrollbarRef" @scroll="onScroll">
|
||||
<div class="document-content">
|
||||
<div
|
||||
v-for="(slice, index) in slices"
|
||||
:key="slice.id"
|
||||
class="slice-section relative"
|
||||
:class="{ 'slice-highlight': selectedSliceId === slice.id }"
|
||||
@click="selectSlice(slice)"
|
||||
@dblclick="startEditing(slice)"
|
||||
:id="`slice-${slice.id}`"
|
||||
>
|
||||
<!-- Slice info - only shown when selected, positioned at top-right -->
|
||||
<div
|
||||
v-if="selectedSliceId === slice.id"
|
||||
class="slice-info absolute top-2 right-2 z-10 bg-white dark:bg-gray-800 rounded-md shadow-sm px-4 py-3 border border-gray-200"
|
||||
>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<span v-if="selectedSlice?.sliceStatus === '1'" class="px-2 py-1 bg-primary text-white text-xs font-bold rounded">
|
||||
已训练
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="w-4 h-4 mr-1 text-gray-500"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M639.2 282.6l80.2-80.6c5.9-5.6 15.3-5.6 20.7 0 2.3 2.3 3.3 4.3 4 6.6V209.2l15 55.7 55.7 15 0.3 0.2h0.2c2.3 0.7 4.7 1.6 6.4 3.8 5.7 5.6 5.7 14.8 0 20.7l-80 80.2c-3.8 3.7-9 5.2-14.4 3.8l-45.1-12-23 23.3c25.1 32 39.8 72.9 39.8 117.2 0 52.5-21.4 100.7-56.2 135.5l-0.7 0.5c-35 34.8-82.4 56-135 56-52.9 0-101.1-21.6-135.7-56.5-34.8-34.8-56.4-83-56.4-135.5 0-53.1 21.6-100.9 56.4-135.9C406 346.4 454.2 325 507.1 325c44.2 0 84.9 14.8 117.3 39.8l23.1-22.8-12.2-45.4c-1.5-4.9 0.2-10.5 3.9-14z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
<span>命中: {{ selectedSlice?.hitCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="mr-1 w-5 h-5"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M109.568 99.328s-17.408 15.36 0 38.912 295.936 392.192 295.936 392.192 17.408 15.36 0 35.84-288.768 323.584-288.768 323.584-20.48 32.768 3.072 32.768h733.184s27.648 0 33.792-29.696c7.168-29.696 33.792-154.624 33.792-154.624s0-12.288-10.24-17.408c-10.24-6.144-30.72 0-33.792 9.216-3.072 9.216-92.16 98.304-125.952 104.448h-471.04l237.568-273.408s17.408-12.288 17.408-29.696-23.552-45.056-23.552-45.056L276.48 169.984h458.752s88.064 47.104 146.432 136.192c7.168 9.216 40.96 9.216 40.96 0s-51.2-201.728-51.2-201.728l-761.856-5.12z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
<span>字符: {{ selectedSlice?.charCount || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-3 gap-3">
|
||||
<el-button size="small" class="flex items-center" type="primary" @click="startEditing(slice)" text>
|
||||
<el-icon class="mr-1"><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button size="small" class="flex items-center" type="warning" @click="handleRetrain(slice)" text v-if="slice.sliceStatus === '1'">
|
||||
<el-icon class="mr-1"><Refresh /></el-icon>重新训练
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示内容或编辑框 -->
|
||||
<div v-if="!editingSlice || editingSlice.id !== slice.id">
|
||||
<MdRenderer :source="slice.content" :key="`md-${slice.id}`" />
|
||||
</div>
|
||||
<el-input
|
||||
v-else
|
||||
type="textarea"
|
||||
v-model="editingContent"
|
||||
:autosize="{ minRows: 6, maxRows: 10 }"
|
||||
@blur="saveContent(slice)"
|
||||
ref="editInputRef"
|
||||
class="slice-editor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="slices.length === 0" class="text-center py-10 text-gray-500">
|
||||
当前文档暂无切片内容
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-center py-4">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span class="ml-2">加载中...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="noMoreData && slices.length > 0" class="text-center py-4 text-gray-400 text-sm">
|
||||
没有更多数据了
|
||||
</div>
|
||||
|
||||
<!-- Add a loading trigger element at the bottom -->
|
||||
<div v-if="!noMoreData && !loading" class="loading-trigger h-10" ref="loadTriggerRef"></div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 文档关键词选项卡 -->
|
||||
<el-tab-pane label="文档关键词" name="keywords" class="flex-1">
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div v-if="!props.documentId" class="text-center text-gray-500">
|
||||
<el-icon class="text-2xl mb-2"><Picture /></el-icon>
|
||||
<div>请选择文档</div>
|
||||
</div>
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<img
|
||||
:src="`${baseURL}${other.adaptationUrl('/knowledge/aiDocument/wordcloud')}?documentId=${props.documentId}`"
|
||||
alt="关键词云图"
|
||||
class="max-w-full max-h-full object-contain"
|
||||
style="max-height: calc(100vh - 200px);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchList } from '/@/api/knowledge/aiSlice';
|
||||
import { getObj as getDocument } from '/@/api/knowledge/aiDocument';
|
||||
import { useMessage, useMessageBox } from '/@/hooks/message';
|
||||
import MdRenderer from '/@/components/MdRenderer/MdRenderer.vue';
|
||||
import { Loading, Edit, Refresh, Picture } from '@element-plus/icons-vue';
|
||||
import { putObj } from '/@/api/knowledge/aiSlice';
|
||||
import { useIntersectionObserver } from '@vueuse/core';
|
||||
import other from '/@/utils/other';
|
||||
|
||||
const props = defineProps({
|
||||
documentId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
sliceId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
const documentTitle = ref('文档查看');
|
||||
const slices = ref<any[]>([]);
|
||||
const selectedSliceId = ref<string | null>(null);
|
||||
const selectedSlice = ref<any>(null);
|
||||
const scrollbarRef = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
const noMoreData = ref(false);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(5);
|
||||
const totalSlices = ref(0);
|
||||
const allSlicesLoaded = ref(false);
|
||||
// 编辑相关变量
|
||||
const editingSlice = ref<any>(null);
|
||||
const editingContent = ref('');
|
||||
const editInputRef = ref<HTMLElement | null>(null);
|
||||
const loadTriggerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// 选项卡和关键词云图相关状态
|
||||
const activeTab = ref('document');
|
||||
const imageError = ref('');
|
||||
|
||||
// 获取文档切片列表
|
||||
const getDocumentSlices = async (page = 1, append = false) => {
|
||||
if (!props.documentId || loading.value) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 获取文档信息(仅在第一页时获取)
|
||||
if (page === 1) {
|
||||
const documentRes = await getDocument(props.documentId);
|
||||
if (documentRes && documentRes.data) {
|
||||
documentTitle.value = documentRes.data.name || '未命名文档';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取该文档的切片(分页)
|
||||
const { data } = await fetchList({
|
||||
documentId: props.documentId,
|
||||
current: page,
|
||||
size: pageSize.value,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
totalSlices.value = data.total;
|
||||
|
||||
// 处理切片数据
|
||||
const newSlices = data.records.sort((a: any, b: any) => {
|
||||
// 如果有顺序字段使用顺序字段,否则按ID排序
|
||||
return (a.orderNum || 0) - (b.orderNum || 0);
|
||||
});
|
||||
|
||||
// 如果返回的记录为空,设置没有更多数据标志
|
||||
if (newSlices.length === 0) {
|
||||
noMoreData.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 追加或替换数据
|
||||
if (append) {
|
||||
slices.value = [...slices.value, ...newSlices];
|
||||
} else {
|
||||
slices.value = newSlices;
|
||||
}
|
||||
|
||||
// 检查是否还有更多数据
|
||||
noMoreData.value = slices.value.length >= totalSlices.value;
|
||||
allSlicesLoaded.value = slices.value.length >= totalSlices.value;
|
||||
}
|
||||
} catch (error: any) {
|
||||
useMessage().error(error.msg || '获取文档切片失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多数据
|
||||
const loadMoreSlices = () => {
|
||||
if (loading.value || noMoreData.value) return;
|
||||
|
||||
currentPage.value++;
|
||||
getDocumentSlices(currentPage.value, true);
|
||||
};
|
||||
|
||||
// 滚动事件处理
|
||||
const onScroll = ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number, scrollHeight: number, clientHeight: number }) => {
|
||||
// 当滚动到底部附近时加载更多数据
|
||||
if (scrollHeight - scrollTop - clientHeight < 200 && !loading.value && !noMoreData.value) {
|
||||
loadMoreSlices();
|
||||
}
|
||||
};
|
||||
|
||||
// 使用 useIntersectionObserver 监听加载触发器元素
|
||||
const { stop: stopObserver } = useIntersectionObserver(
|
||||
loadTriggerRef,
|
||||
([{ isIntersecting }]) => {
|
||||
if (isIntersecting && !loading.value && !noMoreData.value && visible.value && slices.value.length > 0) {
|
||||
loadMoreSlices();
|
||||
}
|
||||
},
|
||||
{
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
// 组件卸载时停止观察
|
||||
onBeforeUnmount(() => {
|
||||
stopObserver();
|
||||
});
|
||||
|
||||
// 选择切片
|
||||
const selectSlice = (slice: any) => {
|
||||
if (selectedSliceId.value === slice.id) {
|
||||
selectedSliceId.value = null;
|
||||
selectedSlice.value = null;
|
||||
} else {
|
||||
selectedSliceId.value = slice.id;
|
||||
selectedSlice.value = slice;
|
||||
}
|
||||
};
|
||||
|
||||
// 开始编辑
|
||||
const startEditing = (slice: any) => {
|
||||
editingSlice.value = slice;
|
||||
editingContent.value = slice.content;
|
||||
nextTick(() => {
|
||||
// Use setTimeout to ensure the DOM is updated
|
||||
setTimeout(() => {
|
||||
const textareaElement = document.querySelector('.slice-editor textarea');
|
||||
if (textareaElement) {
|
||||
(textareaElement as HTMLTextAreaElement).focus();
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
// 保存编辑内容
|
||||
const saveContent = async (slice: any) => {
|
||||
// 内容没有变动
|
||||
if (editingContent.value === slice.content) {
|
||||
editingSlice.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 内容为空
|
||||
if (!editingContent.value.trim()) {
|
||||
useMessage().error('内容不能为空');
|
||||
editingContent.value = slice.content;
|
||||
editingSlice.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await putObj({
|
||||
...slice,
|
||||
content: editingContent.value
|
||||
});
|
||||
useMessage().success('修改成功');
|
||||
|
||||
// 更新本地数据
|
||||
const index = slices.value.findIndex(s => s.id === slice.id);
|
||||
if (index !== -1) {
|
||||
slices.value[index].content = editingContent.value;
|
||||
}
|
||||
|
||||
// 如果是当前选中的切片,也更新选中的切片
|
||||
if (selectedSlice.value && selectedSlice.value.id === slice.id) {
|
||||
selectedSlice.value.content = editingContent.value;
|
||||
}
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
} finally {
|
||||
editingSlice.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 重新训练操作
|
||||
const handleRetrain = async (slice: any) => {
|
||||
try {
|
||||
await useMessageBox().confirm('确认要重新训练该切片吗?');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await putObj({ ...slice, sliceStatus: '0' });
|
||||
useMessage().success('已提交重新训练');
|
||||
|
||||
// 更新本地数据
|
||||
const index = slices.value.findIndex(s => s.id === slice.id);
|
||||
if (index !== -1) {
|
||||
slices.value[index].sliceStatus = '0';
|
||||
}
|
||||
|
||||
// 如果是当前选中的切片,也更新选中的切片
|
||||
if (selectedSlice.value && selectedSlice.value.id === slice.id) {
|
||||
selectedSlice.value.sliceStatus = '0';
|
||||
}
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 滚动到指定切片的位置
|
||||
const scrollToSlice = async (sliceId: string) => {
|
||||
// 如果没有找到切片,可能需要加载更多数据
|
||||
if (!slices.value.some(s => s.id === sliceId) && !allSlicesLoaded.value) {
|
||||
// 重置并加载所有数据
|
||||
currentPage.value = 1;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 获取所有切片数据以确保能找到目标切片
|
||||
const { data } = await fetchList({
|
||||
documentId: props.documentId,
|
||||
size: 999, // 大数值以获取所有切片
|
||||
});
|
||||
|
||||
if (data && data.records) {
|
||||
slices.value = data.records.sort((a: any, b: any) => {
|
||||
return (a.orderNum || 0) - (b.orderNum || 0);
|
||||
});
|
||||
allSlicesLoaded.value = true;
|
||||
noMoreData.value = true;
|
||||
}
|
||||
} catch (error: any) {
|
||||
useMessage().error(error.msg || '获取文档切片失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const element = document.getElementById(`slice-${sliceId}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 重置分页状态
|
||||
const resetPagination = () => {
|
||||
currentPage.value = 1;
|
||||
noMoreData.value = false;
|
||||
allSlicesLoaded.value = false;
|
||||
slices.value = [];
|
||||
};
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => visible.value, (newVal) => {
|
||||
if (newVal && props.documentId) {
|
||||
resetPagination();
|
||||
getDocumentSlices();
|
||||
|
||||
if (props.sliceId) {
|
||||
selectedSliceId.value = props.sliceId;
|
||||
scrollToSlice(props.sliceId);
|
||||
}
|
||||
} else if (!newVal) {
|
||||
// 关闭抽屉时重置状态
|
||||
activeTab.value = 'document';
|
||||
imageError.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.documentId, (newVal) => {
|
||||
if (newVal && visible.value) {
|
||||
resetPagination();
|
||||
getDocumentSlices();
|
||||
// 重置图片错误状态
|
||||
imageError.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.sliceId, (newVal) => {
|
||||
if (newVal && visible.value) {
|
||||
selectedSliceId.value = newVal;
|
||||
scrollToSlice(newVal);
|
||||
}
|
||||
});
|
||||
|
||||
// Watch slices to update selectedSlice when data is loaded
|
||||
watch(() => slices.value, (newSlices) => {
|
||||
if (selectedSliceId.value && newSlices.length > 0) {
|
||||
const slice = newSlices.find(s => s.id === selectedSliceId.value);
|
||||
if (slice) {
|
||||
selectedSlice.value = slice;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
109
src/views/knowledge/aiSlice/form.vue
Normal file
109
src/views/knowledge/aiSlice/form.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible"
|
||||
:close-on-click-modal="false" draggable>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
|
||||
|
||||
|
||||
<el-form-item label="内容" prop="content">
|
||||
<editor v-model:get-html="form.content" height="500" width="600"/>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="AiSliceDialog">
|
||||
import {useDict} from '/@/hooks/dict';
|
||||
import {useMessage} from "/@/hooks/message";
|
||||
import {getObj, addObj, putObj} from '/@/api/knowledge/aiSlice'
|
||||
import {rule} from '/@/utils/validate';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// 定义变量内容
|
||||
const dataFormRef = ref();
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
// 定义字典
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
units: '',
|
||||
fileSize: '',
|
||||
hitCount: '',
|
||||
charCount: '',
|
||||
content: '',
|
||||
});
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
content: [{required: true, message: '内容不能为空', trigger: 'blur'}, {
|
||||
min: 100,
|
||||
max: 1500,
|
||||
message: '文本长度在 100 - 1500 之间',
|
||||
trigger: 'blur',
|
||||
}]
|
||||
})
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (id: string) => {
|
||||
visible.value = true
|
||||
form.id = ''
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
|
||||
// 获取aiSlice信息
|
||||
if (id) {
|
||||
form.id = id
|
||||
getaiSliceData(id)
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => {
|
||||
});
|
||||
if (!valid) return false;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
form.id ? await putObj(form) : await addObj(form);
|
||||
useMessage().success(form.id ? '修改成功' : '添加成功');
|
||||
visible.value = false;
|
||||
emit('refresh');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 初始化表单数据
|
||||
const getaiSliceData = (id: string) => {
|
||||
// 获取数据
|
||||
loading.value = true
|
||||
getObj(id).then((res: any) => {
|
||||
Object.assign(form, res.data)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog
|
||||
});
|
||||
</script>
|
||||
310
src/views/knowledge/aiSlice/index.vue
Normal file
310
src/views/knowledge/aiSlice/index.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item label="知识库名" prop="title">
|
||||
<el-select placeholder="请选择知识库" v-model="state.queryForm.datasetId">
|
||||
<el-option :key="index" :label="item.name" :value="item.id" v-for="(item, index) in datasetList">
|
||||
{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文件名" prop="name">
|
||||
<el-input placeholder="请输入文件名" v-model="state.queryForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="content">
|
||||
<el-input placeholder="请输入文件内容" v-model="state.queryForm.content" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已训练" prop="fileStatus">
|
||||
<el-radio-group v-model="state.queryForm.sliceStatus">
|
||||
<el-radio :key="index" :label="item.value" border v-for="(item, index) in yes_no_type">{{ item.label }} </el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList"> 查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'knowledge_aiSlice_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
|
||||
<el-scrollbar>
|
||||
<div class="mx-auto mt-4">
|
||||
<div class="px-4">
|
||||
<div class="grid sm:grid-cols-2 sm:gap-x-6 lg:grid-cols-3">
|
||||
<div
|
||||
class="relative p-6 mb-6 bg-gray-100 rounded-lg border dark:border-gray-700 dark:bg-gray-800"
|
||||
v-for="slice in state.dataList"
|
||||
:key="slice.id"
|
||||
>
|
||||
<button v-if="slice.sliceStatus === '1'" class="absolute -top-2 right-0 px-3 rotate-[20deg] border bg-primary text-white font-bold">
|
||||
已训练
|
||||
</button>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex relative items-center w-2/4 group">
|
||||
<h3
|
||||
class="text-base font-semibold text-gray-900 truncate transition-colors duration-300 dark:text-gray-100 hover:text-primary"
|
||||
:title="slice.name"
|
||||
>
|
||||
<tip :content="slice.name" />
|
||||
{{ slice.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<svg
|
||||
t="1710943696783"
|
||||
class="mr-2 w-5 h-5 text-base text-gray-500"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="4358"
|
||||
width="128"
|
||||
height="128"
|
||||
>
|
||||
<path
|
||||
d="M678.68009472 305.67806859H345.31990528c-21.80301018 0-39.64183671 17.83882651-39.64183669 39.64183669v333.31409429c0 21.80301018 17.83882651 39.64183671 39.64183669 39.6418367h333.31409429c21.80301018 0 39.64183671-17.83882651 39.6418367-39.6418367V345.31990528c0.04609516-21.80301018-17.79273135-39.64183671-39.59574155-39.64183669z"
|
||||
fill="#ffffff"
|
||||
p-id="4359"
|
||||
></path>
|
||||
<path
|
||||
d="M873.6 64.4H150.4c-47.3 0-86 38.7-86 86v723.1c0 47.3 38.7 86 86 86h723.1c47.3 0 86-38.7 86-86V150.4c0.1-47.3-38.6-86-85.9-86z m56.9 778.2c0 48.3-39.6 87.9-87.9 87.9H181.4c-48.3 0-87.9-39.6-87.9-87.9V181.4c0-48.3 39.6-87.9 87.9-87.9h661.1c48.3 0 87.9 39.6 87.9 87.9v661.2z"
|
||||
fill="#000000"
|
||||
p-id="4360"
|
||||
></path>
|
||||
<path
|
||||
d="M639.2 282.6l80.2-80.6c5.9-5.6 15.3-5.6 20.7 0 2.3 2.3 3.3 4.3 4 6.6V209.2l15 55.7 55.7 15 0.3 0.2h0.2c2.3 0.7 4.7 1.6 6.4 3.8 5.7 5.6 5.7 14.8 0 20.7l-80 80.2c-3.8 3.7-9 5.2-14.4 3.8l-45.1-12-23 23.3c25.1 32 39.8 72.9 39.8 117.2 0 52.5-21.4 100.7-56.2 135.5l-0.7 0.5c-35 34.8-82.4 56-135 56-52.9 0-101.1-21.6-135.7-56.5-34.8-34.8-56.4-83-56.4-135.5 0-53.1 21.6-100.9 56.4-135.9C406 346.4 454.2 325 507.1 325c44.2 0 84.9 14.8 117.3 39.8l23.1-22.8-12.2-45.4c-1.5-4.9 0.2-10.5 3.9-14z m116.7 159c-4-13.6 3.5-27 16.4-30.8 12.9-4 26.4 3.5 30.4 16.4 4.3 14.4 7.8 29.1 9.9 44.4 2.1 14.6 3.3 30.1 3.3 45.6 0 85.1-34.4 162.5-90.3 218.1-56 55.7-133.3 90.8-218.5 90.8S344.6 791 288.6 735.3C233 679.6 198 602.2 198 517.1c0-85.4 35-162.8 90.6-218.8 56-55.7 133.2-90.1 218.5-90.1 15.1 0 30.6 0.9 45.2 3.5 15.3 1.9 30.3 5.4 44.5 9.6 12.9 3.7 20.2 17.6 16.4 30.6-3.8 12.9-17.7 20.2-30.6 16.4-12.2-3.8-24.7-6.4-37.2-8.4-12.2-1.9-25.1-3-38.3-3-71.8 0-136.7 29.2-183.7 76.5-47.2 46.8-76.2 111.7-76.2 183.7 0 71.5 29.1 136.4 76.2 183.7 47 46.8 111.9 76 183.7 76 71.7 0 136.7-29.2 183.7-76 47-47.3 76.2-112.2 76.2-183.7 0-13.2-1-26.5-2.8-38.3-1.7-12.8-4.5-25-8.3-37.2z m-248.8-12.8c15.3 0 29.9 3.8 42.8 10.8l53.6-53.7c-27.1-19.7-60.4-31.1-96.4-31.1-44.9 0-85.6 17.9-114.8 47.3v-0.2 0.2c-29.6 29.4-47.7 70.1-47.7 115.2 0 44.5 18.1 85.2 47.7 114.6 29.2 29.6 69.9 47.8 114.8 47.8 44.5 0 84.9-18.1 114.1-47l0.7-0.9c29.6-29.2 47.7-70.1 47.7-114.6 0-36.4-11.5-69.8-31.3-96.4L584.4 474c7 13.2 11.1 27.3 11.1 43.1 0 24-10.1 46.3-25.9 62.3h-0.2c-16 15.7-38.1 25.9-62.3 25.9-24.4 0-46.4-10.3-62.3-25.9-16-16-26.1-37.9-26.1-62.3 0-24.5 10.1-46.6 26.1-62.3v-0.4c15.9-15.9 38-25.6 62.3-25.6z m20.4 32.7c-6.1-2.3-13.2-3.5-20.4-3.5-16.4 0-31 6.6-41.7 17.4-10.4 10.4-17.2 25.4-17.2 41.8s6.8 30.8 17.2 41.4c10.8 10.6 25.4 17.4 41.7 17.4 16.4 0 31-6.8 41.6-17v-0.4c10.6-10.6 17.2-25 17.2-41.4 0-7.5-1.2-14.4-3.5-20.5l-37.9 37.8c-9.7 9.4-25 9.4-34.8 0-9.4-9.6-9.4-25.2 0-34.6l37.8-38.4z m194.7-220.3l-56.4 56.2 8.7 31.5 56.4-56.4-8.7-31.3z m29.2 52l-56 56.5 31.3 8.4 56.4-56.2-31.7-8.7z"
|
||||
fill="#000000"
|
||||
p-id="4361"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="mr-1">命中次数:{{ slice.hitCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 双击此区域进入编辑模式,失去光标则边际结束-->
|
||||
|
||||
<div class="overflow-y-auto h-48 rounded hover:bg-white dark:hover:bg-gray-700">
|
||||
<!-- 使用MdRenderer组件显示内容 -->
|
||||
<div v-if="!editingSlice || editingSlice.id !== slice.id" @dblclick="startEditing(slice)" class="h-40">
|
||||
<MdRenderer :source="slice.content" />
|
||||
</div>
|
||||
<!-- 编辑模式 -->
|
||||
<el-input
|
||||
v-else
|
||||
type="textarea"
|
||||
v-model="editingContent"
|
||||
class="h-40"
|
||||
:autosize="{ minRows: 6, maxRows: 10 }"
|
||||
@blur="saveContent(slice)"
|
||||
ref="editInputRef"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
<div class="flex">
|
||||
<svg
|
||||
t="1710943778579"
|
||||
class="mr-1 w-6 h-5"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="8180"
|
||||
width="128"
|
||||
height="128"
|
||||
>
|
||||
<path
|
||||
d="M109.568 99.328s-17.408 15.36 0 38.912 295.936 392.192 295.936 392.192 17.408 15.36 0 35.84-288.768 323.584-288.768 323.584-20.48 32.768 3.072 32.768h733.184s27.648 0 33.792-29.696c7.168-29.696 33.792-154.624 33.792-154.624s0-12.288-10.24-17.408c-10.24-6.144-30.72 0-33.792 9.216-3.072 9.216-92.16 98.304-125.952 104.448h-471.04l237.568-273.408s17.408-12.288 17.408-29.696-23.552-45.056-23.552-45.056L276.48 169.984h458.752s88.064 47.104 146.432 136.192c7.168 9.216 40.96 9.216 40.96 0s-51.2-201.728-51.2-201.728l-761.856-5.12z"
|
||||
fill="#111111"
|
||||
p-id="8181"
|
||||
></path>
|
||||
</svg>
|
||||
字符数:{{ slice.charCount }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<el-button class="!p-0" icon="refresh" @click="handleRetrain(slice)" text type="primary" v-auth="'knowledge_aiSlice_del'"
|
||||
>重新训练
|
||||
</el-button>
|
||||
<el-button class="!p-0" icon="delete" @click="handleDelete([slice.id])" text type="primary" v-auth="'knowledge_aiSlice_del'"
|
||||
>{{ $t('common.delBtn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemAiSlice">
|
||||
import { BasicTableProps, useTable } from '/@/hooks/table';
|
||||
import { fetchList, delObjs, putObj } from '/@/api/knowledge/aiSlice';
|
||||
import { useMessage, useMessageBox } from '/@/hooks/message';
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { fetchDataList } from '/@/api/knowledge/aiDataset';
|
||||
import MdRenderer from '/@/components/MdRenderer/MdRenderer.vue';
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
|
||||
// 定义查询字典
|
||||
const { yes_no_type } = useDict('yes_no_type');
|
||||
const route = useRoute();
|
||||
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref();
|
||||
const editingSlice = ref<any>(null);
|
||||
const editingContent = ref('');
|
||||
const editInputRef = ref<HTMLElement | null>(null);
|
||||
// 搜索变量
|
||||
const queryRef = ref();
|
||||
const showSearch = ref(true);
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any;
|
||||
const multiple = ref(true);
|
||||
const { t } = useI18n();
|
||||
|
||||
const datasetList = ref([]);
|
||||
const getDatasetList = async () => {
|
||||
const { data } = await fetchDataList();
|
||||
datasetList.value = data;
|
||||
};
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
createdIsNeed: false,
|
||||
pageList: fetchList,
|
||||
pagination: {
|
||||
size: 6,
|
||||
pageSizes: [3, 6, 9, 12],
|
||||
},
|
||||
});
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields();
|
||||
state.queryForm = {};
|
||||
// 清空多选
|
||||
selectObjs.value = [];
|
||||
getDataList();
|
||||
};
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile('/knowledge/aiSlice/export', Object.assign(state.queryForm, { ids: selectObjs }), 'aiSlice.xlsx');
|
||||
};
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id);
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm('此操作将永久删除');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await delObjs(ids);
|
||||
getDataList();
|
||||
useMessage().success('删除成功');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始编辑
|
||||
const startEditing = (slice: any) => {
|
||||
editingSlice.value = slice;
|
||||
editingContent.value = slice.content;
|
||||
nextTick(() => {
|
||||
editInputRef.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 保存编辑内容
|
||||
const saveContent = async (slice: any) => {
|
||||
// 内容没有变动
|
||||
if (editingContent.value === slice.content) {
|
||||
editingSlice.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 内容为空
|
||||
if (!editingContent.value.trim()) {
|
||||
useMessage().error('内容不能为空');
|
||||
editingContent.value = slice.content;
|
||||
editingSlice.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await putObj({
|
||||
...slice,
|
||||
content: editingContent.value
|
||||
});
|
||||
useMessage().success('修改成功');
|
||||
getDataList();
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
} finally {
|
||||
editingSlice.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 重新训练操作
|
||||
const handleRetrain = async (slice: any) => {
|
||||
try {
|
||||
await useMessageBox().confirm('确认要重新训练该切片吗?');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await putObj({ ...slice, sliceStatus: '0' });
|
||||
useMessage().success('已提交重新训练');
|
||||
getDataList();
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.documentId) {
|
||||
state.queryForm.documentId = route.query.documentId;
|
||||
}
|
||||
|
||||
// 查询表格数据
|
||||
await getDataList();
|
||||
// 查询知识库列表
|
||||
getDatasetList();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user