fix
This commit is contained in:
@@ -104,9 +104,7 @@
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div v-if="!uploadedImageBase64" class="text-xs text-red-500 mt-1">
|
||||
请上传参考图片
|
||||
</div>
|
||||
<div v-if="!uploadedImageBase64" class="text-xs text-red-500 mt-1">请上传参考图片</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -380,7 +378,7 @@ const generateImage = async () => {
|
||||
ElMessage.warning('请输入提示词!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!uploadedImageBase64.value) {
|
||||
ElMessage.warning('请上传参考图片!');
|
||||
return;
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
<!-- Right chat window -->
|
||||
<div class="flex-1" :class="{ 'w-full': !isDirectChat, 'flex-1': isDirectChat }">
|
||||
<chat-window
|
||||
ref="chatWindowRef"
|
||||
:knowledge-id="selectedKnowledgeId"
|
||||
<chat-window
|
||||
ref="chatWindowRef"
|
||||
:knowledge-id="selectedKnowledgeId"
|
||||
:knowledge-data="selectedKnowledge"
|
||||
:flow-id="props.flowId || (route.query.flowId as string) || ''"
|
||||
/>
|
||||
@@ -113,7 +113,7 @@ onBeforeMount(() => {
|
||||
}
|
||||
} else {
|
||||
selectedKnowledgeId.value = datasetId;
|
||||
selectedKnowledge.value = {id: datasetId};
|
||||
selectedKnowledge.value = { id: datasetId };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,10 +98,7 @@ export const parseWelcomeMessage = (welcomeMsg?: string): PrologueItem[] => {
|
||||
* @param {PrologueItem[]} initialPrologueList - 初始的 prologue 列表
|
||||
* @returns {Promise<PrologueItem[]>} 处理后的 prologue items
|
||||
*/
|
||||
export const processPrologueItems = async (
|
||||
selectedKnowledge: Dataset,
|
||||
initialPrologueList: PrologueItem[]
|
||||
): Promise<PrologueItem[]> => {
|
||||
export const processPrologueItems = async (selectedKnowledge: Dataset, initialPrologueList: PrologueItem[]): Promise<PrologueItem[]> => {
|
||||
// 如果存在 mcpId,从后端获取 MCP 元数据
|
||||
if (selectedKnowledge?.mcpId) {
|
||||
const { data } = await getObj(selectedKnowledge.mcpId);
|
||||
@@ -143,21 +140,21 @@ const token = computed(() => {
|
||||
export const generateConversationKey = (knowledgeId: string, notime?: boolean, mcpId?: string, dataId?: string) => {
|
||||
// 构建基础key
|
||||
let key = `chat-${knowledgeId}-${useUserInfo().userInfos.user.userId}-${token.value}`;
|
||||
|
||||
|
||||
// 如果有mcpId,添加到key中
|
||||
if (mcpId) {
|
||||
key += `-mcp-${mcpId}`;
|
||||
}
|
||||
|
||||
|
||||
// 如果有dataId,添加到key中
|
||||
if (dataId) {
|
||||
key += `-data-${dataId}`;
|
||||
}
|
||||
|
||||
|
||||
// 如果需要时间戳,添加到key末尾
|
||||
if (!notime) {
|
||||
key += `-${Date.now()}`;
|
||||
}
|
||||
|
||||
|
||||
return key;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,9 +82,16 @@
|
||||
v-if="!selectRow.recordId"
|
||||
class="flex flex-col items-center justify-center h-full p-8 bg-white border shadow-lg rounded-2xl border-slate-200/60 shadow-slate-100/50 backdrop-blur-sm"
|
||||
>
|
||||
<div class="flex items-center justify-center w-16 h-16 mb-6 border bg-gradient-to-br from-indigo-50 to-blue-50 rounded-2xl border-indigo-100/50">
|
||||
<div
|
||||
class="flex items-center justify-center w-16 h-16 mb-6 border bg-gradient-to-br from-indigo-50 to-blue-50 rounded-2xl border-indigo-100/50"
|
||||
>
|
||||
<svg class="w-8 h-8 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
@@ -95,7 +102,10 @@
|
||||
<template v-else>
|
||||
<div class="h-full overflow-hidden bg-white border shadow-lg rounded-2xl border-slate-200/60 shadow-slate-100/50 backdrop-blur-sm">
|
||||
<!-- 顶部标注切换区域 -->
|
||||
<div class="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b bg-gradient-to-r from-blue-500/5 to-indigo-500/5 backdrop-blur-md border-blue-100/50" v-if="selectRow.llmFlag === '1'">
|
||||
<div
|
||||
class="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b bg-gradient-to-r from-blue-500/5 to-indigo-500/5 backdrop-blur-md border-blue-100/50"
|
||||
v-if="selectRow.llmFlag === '1'"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="relative">
|
||||
<div class="w-3 h-3 rounded-full bg-gradient-to-r from-blue-500 to-indigo-500 animate-pulse"></div>
|
||||
@@ -105,10 +115,10 @@
|
||||
</div>
|
||||
<div class="flex items-center px-4 py-2 space-x-4 border rounded-full bg-white/60 border-blue-100/50 backdrop-blur-sm">
|
||||
<label class="text-sm font-medium cursor-pointer select-none text-slate-700">标注为正确答案</label>
|
||||
<el-switch
|
||||
v-model="selectRow.standardFlag"
|
||||
@change="editHandle"
|
||||
:active-value="'1'"
|
||||
<el-switch
|
||||
v-model="selectRow.standardFlag"
|
||||
@change="editHandle"
|
||||
:active-value="'1'"
|
||||
:inactive-value="'0'"
|
||||
class="scale-110 drop-shadow-sm"
|
||||
></el-switch>
|
||||
@@ -121,20 +131,31 @@
|
||||
<!-- 用户提问区域 -->
|
||||
<div class="relative group">
|
||||
<div v-if="selectRow.llmFlag === '2'" class="relative">
|
||||
<div class="p-6 transition-all duration-300 border shadow-sm bg-gradient-to-br from-white to-slate-50/30 rounded-2xl border-slate-200/50 hover:shadow-md hover:border-slate-300/50">
|
||||
<div
|
||||
class="p-6 transition-all duration-300 border shadow-sm bg-gradient-to-br from-white to-slate-50/30 rounded-2xl border-slate-200/50 hover:shadow-md hover:border-slate-300/50"
|
||||
>
|
||||
<div class="leading-relaxed prose-sm prose max-w-none text-slate-700" v-html="matchResult" @click="handleChildClick" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="relative">
|
||||
<div class="p-6 transition-all duration-300 border shadow-sm bg-gradient-to-br from-blue-50/30 to-indigo-50/30 rounded-2xl border-blue-200/50 hover:shadow-md hover:border-blue-300/50 hover:bg-gradient-to-br hover:from-blue-50/50 hover:to-indigo-50/50">
|
||||
<div
|
||||
class="p-6 transition-all duration-300 border shadow-sm bg-gradient-to-br from-blue-50/30 to-indigo-50/30 rounded-2xl border-blue-200/50 hover:shadow-md hover:border-blue-300/50 hover:bg-gradient-to-br hover:from-blue-50/50 hover:to-indigo-50/50"
|
||||
>
|
||||
<div class="leading-relaxed prose-sm prose max-w-none text-slate-800 font-medium">
|
||||
{{ selectRow.questionText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<div class="flex items-center px-2 py-1 text-xs font-medium text-blue-600 bg-blue-50/80 rounded-full border border-blue-200/50 backdrop-blur-sm">
|
||||
<div
|
||||
class="flex items-center px-2 py-1 text-xs font-medium text-blue-600 bg-blue-50/80 rounded-full border border-blue-200/50 backdrop-blur-sm"
|
||||
>
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
问题
|
||||
</div>
|
||||
@@ -146,9 +167,16 @@
|
||||
<div class="relative group">
|
||||
<div class="flex items-center mb-5 space-x-3">
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-center w-10 h-10 border shadow-sm bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border-blue-100/50">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 border shadow-sm bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border-blue-100/50"
|
||||
>
|
||||
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute w-3 h-3 border-2 border-white rounded-full shadow-sm -top-1 -right-1 bg-blue-400"></div>
|
||||
@@ -157,16 +185,25 @@
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-slate-800">AI 智能回答</h3>
|
||||
</div>
|
||||
<div class="flex items-center px-3 py-1 border rounded-full bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-100/50">
|
||||
<div
|
||||
class="flex items-center px-3 py-1 border rounded-full bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-100/50"
|
||||
>
|
||||
<svg class="w-3 h-3 text-blue-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-xs font-medium text-blue-600">支持Markdown</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden transition-all duration-300 border shadow-sm bg-gradient-to-br from-white to-slate-50/30 rounded-2xl border-slate-200/50 hover:shadow-md hover:border-slate-300/50">
|
||||
<div
|
||||
class="overflow-hidden transition-all duration-300 border shadow-sm bg-gradient-to-br from-white to-slate-50/30 rounded-2xl border-slate-200/50 hover:shadow-md hover:border-slate-300/50"
|
||||
>
|
||||
<ai-editor
|
||||
v-model="selectRow.answerText"
|
||||
output="text"
|
||||
|
||||
@@ -235,12 +235,7 @@
|
||||
可见范围
|
||||
<tip content="选择可以访问此知识库的用户,如果不选择则全部的用户可访问" />
|
||||
</template>
|
||||
<org-selector
|
||||
v-model="form.visibleUsers"
|
||||
:type="'user'"
|
||||
:multiple="true"
|
||||
:selectSelf="true"
|
||||
/>
|
||||
<org-selector v-model="form.visibleUsers" :type="'user'" :multiple="true" :selectSelf="true" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -1,215 +1,211 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="选择挂载方式"
|
||||
width="50%"
|
||||
:modal="false"
|
||||
>
|
||||
<div>
|
||||
<el-card size="mini">
|
||||
<el-row :gutter="10" justify="center">
|
||||
<el-col :span="8">
|
||||
<div :class="selectedMethod === 1 ? 'img-selected' : 'img-unselect'" @click="selectImg(1)">
|
||||
<div class="css-1awpln7">
|
||||
<div class="css-0">
|
||||
<img alt="" src="/@/assets/ai/link.svg" class="chakra-image css-0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div :class="selectedMethod === 2 ? 'img-selected' : 'img-unselect'" @click="selectImg(2)">
|
||||
<div class="css-1awpln7">
|
||||
<div class="css-0">
|
||||
<img alt="" src="/@/assets/ai/iframe.svg" class="chakra-image css-0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div :class="selectedMethod === 3 ? 'img-selected' : 'img-unselect'" @click="selectImg(3)">
|
||||
<div class="css-1awpln7">
|
||||
<div class="css-0">
|
||||
<img alt="" src="/@/assets/ai/script.svg" class="chakra-image css-0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
<div>
|
||||
<el-card size="mini" body-class="card-class">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div v-if="selectedMethod === 1">
|
||||
<span>将下面链接复制到浏览器打开</span>
|
||||
<span>
|
||||
<el-button style="float: right" class="ml-2" type="primary" @click="openText(url())">打开</el-button>
|
||||
<el-button style="float: right" type="primary" @click="copyText(url())">复制</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectedMethod === 2">
|
||||
<span>复制下面 Iframe 加入到你的网站中</span>
|
||||
<span>
|
||||
<el-button style="float: right" type="primary" @click="copyText(iframe())">复制</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectedMethod === 3">
|
||||
<span>将下面代码加入到你的网站中</span>
|
||||
<span>
|
||||
<el-button style="float: right" type="primary" @click="copyText(script())">复制</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span v-if="selectedMethod === 1">
|
||||
<div class="mockup-code">
|
||||
<pre data-prefix="$"><code> {{ url() }}</code></pre>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="selectedMethod === 2">
|
||||
<div class="mockup-code">
|
||||
<pre data-prefix="$"><code> {{ iframe() }}</code></pre>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="selectedMethod === 3">
|
||||
<div class="mockup-code">
|
||||
<pre data-prefix="$"><code> {{ script() }}</code></pre>
|
||||
</div>
|
||||
</span>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="dialogVisible" title="选择挂载方式" width="50%" :modal="false">
|
||||
<div>
|
||||
<el-card size="mini">
|
||||
<el-row :gutter="10" justify="center">
|
||||
<el-col :span="8">
|
||||
<div :class="selectedMethod === 1 ? 'img-selected' : 'img-unselect'" @click="selectImg(1)">
|
||||
<div class="css-1awpln7">
|
||||
<div class="css-0">
|
||||
<img alt="" src="/@/assets/ai/link.svg" class="chakra-image css-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div :class="selectedMethod === 2 ? 'img-selected' : 'img-unselect'" @click="selectImg(2)">
|
||||
<div class="css-1awpln7">
|
||||
<div class="css-0">
|
||||
<img alt="" src="/@/assets/ai/iframe.svg" class="chakra-image css-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div :class="selectedMethod === 3 ? 'img-selected' : 'img-unselect'" @click="selectImg(3)">
|
||||
<div class="css-1awpln7">
|
||||
<div class="css-0">
|
||||
<img alt="" src="/@/assets/ai/script.svg" class="chakra-image css-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
<div>
|
||||
<el-card size="mini" body-class="card-class">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div v-if="selectedMethod === 1">
|
||||
<span>将下面链接复制到浏览器打开</span>
|
||||
<span>
|
||||
<el-button style="float: right" class="ml-2" type="primary" @click="openText(url())">打开</el-button>
|
||||
<el-button style="float: right" type="primary" @click="copyText(url())">复制</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectedMethod === 2">
|
||||
<span>复制下面 Iframe 加入到你的网站中</span>
|
||||
<span>
|
||||
<el-button style="float: right" type="primary" @click="copyText(iframe())">复制</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectedMethod === 3">
|
||||
<span>将下面代码加入到你的网站中</span>
|
||||
<span>
|
||||
<el-button style="float: right" type="primary" @click="copyText(script())">复制</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span v-if="selectedMethod === 1">
|
||||
<div class="mockup-code">
|
||||
<pre data-prefix="$"><code> {{ url() }}</code></pre>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="selectedMethod === 2">
|
||||
<div class="mockup-code">
|
||||
<pre data-prefix="$"><code> {{ iframe() }}</code></pre>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="selectedMethod === 3">
|
||||
<div class="mockup-code">
|
||||
<pre data-prefix="$"><code> {{ script() }}</code></pre>
|
||||
</div>
|
||||
</span>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="AiDatasetDialog">
|
||||
import commonFunction from "/@/utils/commonFunction";
|
||||
import commonFunction from '/@/utils/commonFunction';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
const {copyText} = commonFunction();
|
||||
const { copyText } = commonFunction();
|
||||
|
||||
const left_right = [
|
||||
{
|
||||
label: '左侧',
|
||||
value: 'left',
|
||||
},
|
||||
{
|
||||
label: '右侧',
|
||||
value: 'right',
|
||||
}
|
||||
]
|
||||
{
|
||||
label: '左侧',
|
||||
value: 'left',
|
||||
},
|
||||
{
|
||||
label: '右侧',
|
||||
value: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
const top_bottom = [
|
||||
{
|
||||
label: '顶部',
|
||||
value: 'top',
|
||||
},
|
||||
{
|
||||
label: '底部',
|
||||
value: 'bottom',
|
||||
}
|
||||
]
|
||||
const dialogVisible = ref(false)
|
||||
const selectedDatasetId = ref()
|
||||
const selectedMethod = ref(1)
|
||||
const data_btn_x = ref(16)
|
||||
const data_btn_y = ref(16)
|
||||
const data_stream = ref(true)
|
||||
const data_direction_x = ref('right')
|
||||
const data_direction_y = ref('bottom')
|
||||
{
|
||||
label: '顶部',
|
||||
value: 'top',
|
||||
},
|
||||
{
|
||||
label: '底部',
|
||||
value: 'bottom',
|
||||
},
|
||||
];
|
||||
const dialogVisible = ref(false);
|
||||
const selectedDatasetId = ref();
|
||||
const selectedMethod = ref(1);
|
||||
const data_btn_x = ref(16);
|
||||
const data_btn_y = ref(16);
|
||||
const data_stream = ref(true);
|
||||
const data_direction_x = ref('right');
|
||||
const data_direction_y = ref('bottom');
|
||||
const openDialog = (datasetid: any) => {
|
||||
dialogVisible.value = true
|
||||
selectedDatasetId.value = datasetid
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
selectedDatasetId.value = datasetid;
|
||||
};
|
||||
|
||||
const currentHostname = window.location.origin;
|
||||
const openImageBase64 = ref("")
|
||||
const closeImageBase64 = ref("")
|
||||
const openImageBase64 = ref('');
|
||||
const closeImageBase64 = ref('');
|
||||
|
||||
function handleOpenIconBeforeUpload(file: Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = e => {
|
||||
// 在这里处理 Base64 数据
|
||||
openImageBase64.value = e.target.result
|
||||
};
|
||||
return false;
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (e) => {
|
||||
// 在这里处理 Base64 数据
|
||||
openImageBase64.value = e.target.result;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleCloseIconBeforeUpload(file: Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = e => {
|
||||
// 在这里处理 Base64 数据
|
||||
closeImageBase64.value = e.target.result
|
||||
};
|
||||
return false;
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (e) => {
|
||||
// 在这里处理 Base64 数据
|
||||
closeImageBase64.value = e.target.result;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const isMicro = import.meta.env.VITE_IS_MICRO === 'true' ? 1 : 0
|
||||
const isMicro = import.meta.env.VITE_IS_MICRO === 'true' ? 1 : 0;
|
||||
|
||||
const script = () => {
|
||||
return `<script id="chatbot-iframe" src="${currentHostname}/bot/embed.min.js?t=${Date.now()}" data-bot-src="${currentHostname}/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat" async defer><\/script>`
|
||||
}
|
||||
return `<script id="chatbot-iframe" src="${currentHostname}/bot/embed.min.js?t=${Date.now()}" data-bot-src="${currentHostname}/bot/index.html#/${isMicro}/${
|
||||
selectedDatasetId.value
|
||||
}/chat" async defer><\/script>`;
|
||||
};
|
||||
|
||||
const iframe = () => {
|
||||
return `<iframe src="${currentHostname}/bot/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat" style="width: 100%; height: 100%;" frameborder="0" allow="microphone"/>`
|
||||
}
|
||||
return `<iframe src="${currentHostname}/bot/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat" style="width: 100%; height: 100%;" frameborder="0" allow="microphone"/>`;
|
||||
};
|
||||
|
||||
const url = () => {
|
||||
return `${currentHostname}/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat`
|
||||
}
|
||||
return `${currentHostname}/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat`;
|
||||
};
|
||||
const selectImg = (index: number) => {
|
||||
selectedMethod.value = index
|
||||
}
|
||||
selectedMethod.value = index;
|
||||
};
|
||||
|
||||
const openText = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.img-unselect {
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1.5px solid rgb(232, 235, 240);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
background: #fbfbfc;
|
||||
padding: 0px !important;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1.5px solid rgb(232, 235, 240);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
background: #fbfbfc;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.img-unselect:hover {
|
||||
border-color: #1dbcd8;
|
||||
border-color: #1dbcd8;
|
||||
}
|
||||
|
||||
.img-selected {
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-style: solid;
|
||||
border-image: initial;
|
||||
border-width: 1.5px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border-color: #1dbcd8;
|
||||
background: #f0f4ff;
|
||||
padding: 0px !important;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-style: solid;
|
||||
border-image: initial;
|
||||
border-width: 1.5px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border-color: #1dbcd8;
|
||||
background: #f0f4ff;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.card-class {
|
||||
background-color: #F4F4F7;
|
||||
background-color: #f4f4f7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="800" :close-on-click-modal="false" draggable :destroy-on-close="true" class="dark:bg-gray-800">
|
||||
<el-dialog
|
||||
:title="form.id ? '编辑' : '新增'"
|
||||
v-model="visible"
|
||||
:width="800"
|
||||
:close-on-click-modal="false"
|
||||
draggable
|
||||
:destroy-on-close="true"
|
||||
class="dark:bg-gray-800"
|
||||
>
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
:model="form"
|
||||
|
||||
@@ -127,12 +127,9 @@
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
|
||||
|
||||
<!-- 文档查看抽屉 -->
|
||||
<document-drawer
|
||||
v-model="documentDrawerVisible"
|
||||
:document-id="selectedDocumentId"
|
||||
/>
|
||||
<document-drawer v-model="documentDrawerVisible" :document-id="selectedDocumentId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -274,7 +271,7 @@ const viewDocument = (document: any) => {
|
||||
useMessage().warning('只有切片成功的文档才能查看');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
selectedDocumentId.value = document.id;
|
||||
documentDrawerVisible.value = true;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
<template>
|
||||
<el-col class="mb20">
|
||||
<el-form-item label="资料" prop="files">
|
||||
<upload-file
|
||||
:limit="1"
|
||||
@change="handleFileChange"
|
||||
:fileType="['xlsx']"
|
||||
/>
|
||||
<a class="link link-primary" @click="downloadTemplate">Q&A Excel 模板下载</a>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col class="mb20">
|
||||
<el-form-item label="资料" prop="files">
|
||||
<upload-file :limit="1" @change="handleFileChange" :fileType="['xlsx']" />
|
||||
<a class="link link-primary" @click="downloadTemplate">Q&A Excel 模板下载</a>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { downBlobFile } from "/@/utils/other";
|
||||
import { downBlobFile } from '/@/utils/other';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<any>,
|
||||
required: true
|
||||
}
|
||||
modelValue: {
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const handleFileChange = (fileNames: string, fileList: any[]) => {
|
||||
emit('update:modelValue', fileList);
|
||||
}
|
||||
emit('update:modelValue', fileList);
|
||||
};
|
||||
|
||||
const downloadTemplate = () => {
|
||||
downBlobFile('/admin/sys-file/local/file/qa.xlsx', {}, 'Q&A.xlsx');
|
||||
downBlobFile('/admin/sys-file/local/file/qa.xlsx', {}, 'Q&A.xlsx');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -6,12 +6,7 @@
|
||||
</el-col>
|
||||
<el-col class="mb20">
|
||||
<el-form-item label="内容" prop="content">
|
||||
<ai-editor
|
||||
v-model="modelValue.content"
|
||||
output="text"
|
||||
placeholder="选择输入文本,即可调用 AI 辅助功能"
|
||||
:minHeight="400"
|
||||
/>
|
||||
<ai-editor v-model="modelValue.content" output="text" placeholder="选择输入文本,即可调用 AI 辅助功能" :minHeight="400" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
@@ -242,7 +242,7 @@ const onSubmit = async () => {
|
||||
loading.value = true;
|
||||
// 修复TypeScript错误:正确处理apiKey的类型
|
||||
const submitForm = { ...form };
|
||||
|
||||
|
||||
// 处理 pgvector 类型的特殊逻辑
|
||||
if (form.storeType === 'pgvector') {
|
||||
// 将 pgvector 的配置信息存储到 extData 中
|
||||
@@ -253,7 +253,7 @@ const onSubmit = async () => {
|
||||
dimension: form.pgDimension,
|
||||
};
|
||||
submitForm.extData = JSON.stringify(pgConfig);
|
||||
|
||||
|
||||
// 清空不需要的字段
|
||||
submitForm.apiKey = '';
|
||||
} else {
|
||||
@@ -262,13 +262,13 @@ const onSubmit = async () => {
|
||||
submitForm.apiKey = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 移除 pgvector 特有的临时字段
|
||||
delete (submitForm as any).pgUsername;
|
||||
delete (submitForm as any).pgPassword;
|
||||
delete (submitForm as any).pgDatabase;
|
||||
delete (submitForm as any).pgDimension;
|
||||
|
||||
|
||||
form.storeId ? await putObj(submitForm) : await addObj(submitForm);
|
||||
useMessage().success(form.storeId ? '修改成功' : '添加成功');
|
||||
visible.value = false;
|
||||
@@ -287,7 +287,7 @@ const getaiEmbedStoreData = (id: string) => {
|
||||
getObj({ storeId: id })
|
||||
.then((res: any) => {
|
||||
Object.assign(form, res.data);
|
||||
|
||||
|
||||
// 如果是 pgvector 类型,解析 extData 中的配置
|
||||
if (form.storeType === 'pgvector' && form.extData) {
|
||||
try {
|
||||
|
||||
@@ -1,173 +1,173 @@
|
||||
<template>
|
||||
<div v-if="visible"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
left: position.x + 'px',
|
||||
top: position.y + 'px',
|
||||
zIndex: 1000
|
||||
}">
|
||||
<!-- <div class="menu-item"
|
||||
<div
|
||||
v-if="visible"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
left: position.x + 'px',
|
||||
top: position.y + 'px',
|
||||
zIndex: 1000,
|
||||
}"
|
||||
>
|
||||
<!-- <div class="menu-item"
|
||||
@click="handleCommand('addNode')">
|
||||
<el-icon class="menu-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
新增节点
|
||||
</div> -->
|
||||
<div class="menu-item"
|
||||
@click="handleCommand('importDSL')">
|
||||
<el-icon class="menu-icon">
|
||||
<Upload />
|
||||
</el-icon>
|
||||
导入DSL
|
||||
</div>
|
||||
<div class="menu-item"
|
||||
@click="handleCommand('exportDSL')">
|
||||
<el-icon class="menu-icon">
|
||||
<Download />
|
||||
</el-icon>
|
||||
导出DSL
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item" @click="handleCommand('importDSL')">
|
||||
<el-icon class="menu-icon">
|
||||
<Upload />
|
||||
</el-icon>
|
||||
导入DSL
|
||||
</div>
|
||||
<div class="menu-item" @click="handleCommand('exportDSL')">
|
||||
<el-icon class="menu-icon">
|
||||
<Download />
|
||||
</el-icon>
|
||||
导出DSL
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, defineComponent, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Plus, Upload, Download } from '@element-plus/icons-vue'
|
||||
import { ref, defineComponent, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { Plus, Upload, Download } from '@element-plus/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CanvasContextMenu',
|
||||
components: {
|
||||
Plus,
|
||||
Upload,
|
||||
Download
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 })
|
||||
}
|
||||
},
|
||||
emits: ['update:visible', 'add-node', 'import-dsl', 'export-dsl'],
|
||||
setup (props, { emit }) {
|
||||
// 处理菜单命令
|
||||
const handleCommand = (command) => {
|
||||
switch (command) {
|
||||
case 'addNode':
|
||||
emit('add')
|
||||
break
|
||||
case 'importDSL':
|
||||
emit('import')
|
||||
break
|
||||
case 'exportDSL':
|
||||
emit('export')
|
||||
break
|
||||
}
|
||||
emit('update:visible', false)
|
||||
}
|
||||
name: 'CanvasContextMenu',
|
||||
components: {
|
||||
Plus,
|
||||
Upload,
|
||||
Download,
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
},
|
||||
emits: ['update:visible', 'add-node', 'import-dsl', 'export-dsl'],
|
||||
setup(props, { emit }) {
|
||||
// 处理菜单命令
|
||||
const handleCommand = (command) => {
|
||||
switch (command) {
|
||||
case 'addNode':
|
||||
emit('add');
|
||||
break;
|
||||
case 'importDSL':
|
||||
emit('import');
|
||||
break;
|
||||
case 'exportDSL':
|
||||
emit('export');
|
||||
break;
|
||||
}
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
// 点击外部关闭菜单的处理函数
|
||||
const handleClickOutside = (e) => {
|
||||
const contextMenu = document.querySelector('.context-menu')
|
||||
if (contextMenu && !contextMenu.contains(e.target)) {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
// 点击外部关闭菜单的处理函数
|
||||
const handleClickOutside = (e) => {
|
||||
const contextMenu = document.querySelector('.context-menu');
|
||||
if (contextMenu && !contextMenu.contains(e.target)) {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时添加点击事件监听
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
// 组件挂载时添加点击事件监听
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
// 组件销毁前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
// 组件销毁前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
return {
|
||||
handleCommand,
|
||||
}
|
||||
}
|
||||
})
|
||||
return {
|
||||
handleCommand,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context-menu {
|
||||
opacity: 0.9;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
padding: 6px 0;
|
||||
min-width: 180px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
animation: menuFadeIn 0.15s ease-out;
|
||||
opacity: 0.9;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
padding: 6px 0;
|
||||
min-width: 180px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
animation: menuFadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: rgb(71 84 103 / 1);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: rgb(71 84 103 / 1);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f8fafc;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
&:hover {
|
||||
background-color: #f8fafc;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.node-types {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
|
||||
.el-radio {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.el-radio {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-dialog__header {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.el-dialog__header {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,451 +1,442 @@
|
||||
<template>
|
||||
<div class="check-list-panel">
|
||||
<div class="preview-content">
|
||||
<div class="preview-header">
|
||||
<div class="header-title">
|
||||
<span>检查清单({{ validation.errors.length }})</span>
|
||||
</div>
|
||||
<el-button
|
||||
class="close-btn"
|
||||
type="primary"
|
||||
link
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="check-list">
|
||||
<template v-if="validation.errors.length > 0">
|
||||
<div v-for="(error, index) in validation.errors"
|
||||
:key="index"
|
||||
class="check-item">
|
||||
<div class="item-icon warning">
|
||||
<el-icon><Warning /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-type">{{ getErrorType(error) }}</div>
|
||||
<div class="item-message">{{ getErrorMessage(error) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else
|
||||
class="check-item success">
|
||||
<div class="item-icon success">
|
||||
<el-icon><Select /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-type success">检测通过</div>
|
||||
<div class="item-message">工作流程检测已通过,可以发布</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-list-panel">
|
||||
<div class="preview-content">
|
||||
<div class="preview-header">
|
||||
<div class="header-title">
|
||||
<span>检查清单({{ validation.errors.length }})</span>
|
||||
</div>
|
||||
<el-button class="close-btn" type="primary" link @click="$emit('close')">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="check-list">
|
||||
<template v-if="validation.errors.length > 0">
|
||||
<div v-for="(error, index) in validation.errors" :key="index" class="check-item">
|
||||
<div class="item-icon warning">
|
||||
<el-icon><Warning /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-type">{{ getErrorType(error) }}</div>
|
||||
<div class="item-message">{{ getErrorMessage(error) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="check-item success">
|
||||
<div class="item-icon success">
|
||||
<el-icon><Select /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-type success">检测通过</div>
|
||||
<div class="item-message">工作流程检测已通过,可以发布</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Warning, Close, Select } from '@element-plus/icons-vue'
|
||||
import { Warning, Close, Select } from '@element-plus/icons-vue';
|
||||
|
||||
export default {
|
||||
name: 'CheckListPanel',
|
||||
inject: ['parent'],
|
||||
components: {
|
||||
Warning,
|
||||
Close,
|
||||
Select
|
||||
},
|
||||
props: {
|
||||
validation: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:validation', 'close'],
|
||||
data () {
|
||||
return {
|
||||
showCheckList: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'parent.nodes': {
|
||||
handler: 'checkChanges',
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
'parent.connections': {
|
||||
handler: 'checkChanges',
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkChanges () {
|
||||
const newValidation = this.validateWorkflow()
|
||||
this.$emit('update:validation', newValidation)
|
||||
},
|
||||
validateWorkflow () {
|
||||
const validation = {
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
name: 'CheckListPanel',
|
||||
inject: ['parent'],
|
||||
components: {
|
||||
Warning,
|
||||
Close,
|
||||
Select,
|
||||
},
|
||||
props: {
|
||||
validation: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:validation', 'close'],
|
||||
data() {
|
||||
return {
|
||||
showCheckList: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'parent.nodes': {
|
||||
handler: 'checkChanges',
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
'parent.connections': {
|
||||
handler: 'checkChanges',
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkChanges() {
|
||||
const newValidation = this.validateWorkflow();
|
||||
this.$emit('update:validation', newValidation);
|
||||
},
|
||||
validateWorkflow() {
|
||||
const validation = {
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
try {
|
||||
if (!this.parent.nodes || this.parent.nodes.length === 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流中没有节点'
|
||||
});
|
||||
validation.isValid = false;
|
||||
return validation;
|
||||
}
|
||||
try {
|
||||
if (!this.parent.nodes || this.parent.nodes.length === 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流中没有节点',
|
||||
});
|
||||
validation.isValid = false;
|
||||
return validation;
|
||||
}
|
||||
|
||||
const startNodes = this.parent.nodes.filter(node => node.type === 'start');
|
||||
const endNodes = this.parent.nodes.filter(node => node.type === 'end');
|
||||
const startNodes = this.parent.nodes.filter((node) => node.type === 'start');
|
||||
const endNodes = this.parent.nodes.filter((node) => node.type === 'end');
|
||||
|
||||
if (startNodes.length === 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流缺少开始节点'
|
||||
});
|
||||
validation.isValid = false;
|
||||
} else if (startNodes.length > 1) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流只能有一个开始节点'
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
if (startNodes.length === 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流缺少开始节点',
|
||||
});
|
||||
validation.isValid = false;
|
||||
} else if (startNodes.length > 1) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流只能有一个开始节点',
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
if (endNodes.length === 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流缺少结束节点'
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
if (endNodes.length === 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流缺少结束节点',
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
const nodeConnections = new Map();
|
||||
this.parent.nodes.forEach(node => {
|
||||
nodeConnections.set(node.id, {
|
||||
inbound: [],
|
||||
outbound: []
|
||||
});
|
||||
});
|
||||
const nodeConnections = new Map();
|
||||
this.parent.nodes.forEach((node) => {
|
||||
nodeConnections.set(node.id, {
|
||||
inbound: [],
|
||||
outbound: [],
|
||||
});
|
||||
});
|
||||
|
||||
this.parent.connections.forEach(conn => {
|
||||
const sourceConn = nodeConnections.get(conn.sourceId);
|
||||
const targetConn = nodeConnections.get(conn.targetId);
|
||||
this.parent.connections.forEach((conn) => {
|
||||
const sourceConn = nodeConnections.get(conn.sourceId);
|
||||
const targetConn = nodeConnections.get(conn.targetId);
|
||||
|
||||
if (sourceConn) {
|
||||
sourceConn.outbound.push(conn.targetId);
|
||||
}
|
||||
if (targetConn) {
|
||||
targetConn.inbound.push(conn.sourceId);
|
||||
}
|
||||
});
|
||||
if (sourceConn) {
|
||||
sourceConn.outbound.push(conn.targetId);
|
||||
}
|
||||
if (targetConn) {
|
||||
targetConn.inbound.push(conn.sourceId);
|
||||
}
|
||||
});
|
||||
|
||||
this.parent.nodes.forEach(node => {
|
||||
const nodeConn = nodeConnections.get(node.id);
|
||||
this.parent.nodes.forEach((node) => {
|
||||
const nodeConn = nodeConnections.get(node.id);
|
||||
|
||||
if (node.type === 'start' && nodeConn.inbound.length > 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '开始节点不能有入边连接',
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
if (node.type === 'start' && nodeConn.inbound.length > 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '开始节点不能有入边连接',
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
if (node.type === 'end' && nodeConn.outbound.length > 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '结束节点不能有出边连接',
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
if (node.type === 'end' && nodeConn.outbound.length > 0) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '结束节点不能有出边连接',
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
if (nodeConn.inbound.length === 0 && nodeConn.outbound.length === 0 &&
|
||||
node.type !== 'start' && node.type !== 'end') {
|
||||
validation.warnings.push({
|
||||
type: 'warning',
|
||||
message: '存在孤立节点',
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
});
|
||||
if (nodeConn.inbound.length === 0 && nodeConn.outbound.length === 0 && node.type !== 'start' && node.type !== 'end') {
|
||||
validation.warnings.push({
|
||||
type: 'warning',
|
||||
message: '存在孤立节点',
|
||||
nodeId: node.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.parent.nodes.forEach(node => {
|
||||
// switch (node.type) {
|
||||
// case 'http':
|
||||
// this.validateHttpNode(node, validation);
|
||||
// break;
|
||||
// case 'code':
|
||||
// this.validateCodeNode(node, validation);
|
||||
// break;
|
||||
// }
|
||||
});
|
||||
this.parent.nodes.forEach((node) => {
|
||||
// switch (node.type) {
|
||||
// case 'http':
|
||||
// this.validateHttpNode(node, validation);
|
||||
// break;
|
||||
// case 'code':
|
||||
// this.validateCodeNode(node, validation);
|
||||
// break;
|
||||
// }
|
||||
});
|
||||
|
||||
if (this.hasCircularDependency()) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流中存在循环依赖'
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
if (this.hasCircularDependency()) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '工作流中存在循环依赖',
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
return validation;
|
||||
} catch (error) {
|
||||
console.error('验证工作流时出错:', error);
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '验证工作流时出错: ' + error.message
|
||||
});
|
||||
validation.isValid = false;
|
||||
return validation;
|
||||
}
|
||||
},
|
||||
validateHttpNode (node, validation) {
|
||||
if (!node.url) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: 'HTTP节点缺少URL',
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
return validation;
|
||||
} catch (error) {
|
||||
console.error('验证工作流时出错:', error);
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '验证工作流时出错: ' + error.message,
|
||||
});
|
||||
validation.isValid = false;
|
||||
return validation;
|
||||
}
|
||||
},
|
||||
validateHttpNode(node, validation) {
|
||||
if (!node.url) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: 'HTTP节点缺少URL',
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
if (!node.method) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: 'HTTP节点缺少请求方法',
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
if (!node.method) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: 'HTTP节点缺少请求方法',
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
if (node.headers) {
|
||||
node.headers.forEach((header, index) => {
|
||||
if (!header.name) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: `HTTP节点的第${index + 1}个请求头缺少名称`,
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (node.headers) {
|
||||
node.headers.forEach((header, index) => {
|
||||
if (!header.name) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: `HTTP节点的第${index + 1}个请求头缺少名称`,
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (node.method !== 'GET' && node.bodyParams) {
|
||||
node.bodyParams.forEach((param, index) => {
|
||||
if (!param.name) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: `HTTP节点的第${index + 1}个请求体参数缺少名称`,
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
validateCodeNode (node, validation) {
|
||||
if (!node.code) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '代码节点缺少执行代码',
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
} else {
|
||||
try {
|
||||
new Function(node.code);
|
||||
} catch (error) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: `代码语法错误: ${error.message}`,
|
||||
nodeId: node.id
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
hasCircularDependency () {
|
||||
const visited = new Set();
|
||||
const recursionStack = new Set();
|
||||
if (node.method !== 'GET' && node.bodyParams) {
|
||||
node.bodyParams.forEach((param, index) => {
|
||||
if (!param.name) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: `HTTP节点的第${index + 1}个请求体参数缺少名称`,
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
validateCodeNode(node, validation) {
|
||||
if (!node.code) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: '代码节点缺少执行代码',
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
} else {
|
||||
try {
|
||||
new Function(node.code);
|
||||
} catch (error) {
|
||||
validation.errors.push({
|
||||
type: 'error',
|
||||
message: `代码语法错误: ${error.message}`,
|
||||
nodeId: node.id,
|
||||
});
|
||||
validation.isValid = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
hasCircularDependency() {
|
||||
const visited = new Set();
|
||||
const recursionStack = new Set();
|
||||
|
||||
const dfs = (nodeId) => {
|
||||
visited.add(nodeId);
|
||||
recursionStack.add(nodeId);
|
||||
const dfs = (nodeId) => {
|
||||
visited.add(nodeId);
|
||||
recursionStack.add(nodeId);
|
||||
|
||||
const connections = this.parent.connections.filter(conn => conn.sourceId === nodeId);
|
||||
for (const conn of connections) {
|
||||
const targetId = conn.targetId;
|
||||
const connections = this.parent.connections.filter((conn) => conn.sourceId === nodeId);
|
||||
for (const conn of connections) {
|
||||
const targetId = conn.targetId;
|
||||
|
||||
if (!visited.has(targetId)) {
|
||||
if (dfs(targetId)) {
|
||||
return true;
|
||||
}
|
||||
} else if (recursionStack.has(targetId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!visited.has(targetId)) {
|
||||
if (dfs(targetId)) {
|
||||
return true;
|
||||
}
|
||||
} else if (recursionStack.has(targetId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.delete(nodeId);
|
||||
return false;
|
||||
};
|
||||
recursionStack.delete(nodeId);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const node of this.parent.nodes) {
|
||||
if (!visited.has(node.id)) {
|
||||
if (dfs(node.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const node of this.parent.nodes) {
|
||||
if (!visited.has(node.id)) {
|
||||
if (dfs(node.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getErrorType (error) {
|
||||
if (error.message.includes('缺少')) return '节点缺失'
|
||||
if (error.message.includes('未连接')) return '连接错误'
|
||||
if (error.message.includes('循环')) return '循环依赖'
|
||||
return '错误'
|
||||
},
|
||||
getErrorMessage (error) {
|
||||
let nodeName = ''
|
||||
if (error.nodeId) {
|
||||
const node = this.parent.nodes.find(n => n.id === error.nodeId)
|
||||
if (node) {
|
||||
nodeName = node.name || `${node.type}节点`
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getErrorType(error) {
|
||||
if (error.message.includes('缺少')) return '节点缺失';
|
||||
if (error.message.includes('未连接')) return '连接错误';
|
||||
if (error.message.includes('循环')) return '循环依赖';
|
||||
return '错误';
|
||||
},
|
||||
getErrorMessage(error) {
|
||||
let nodeName = '';
|
||||
if (error.nodeId) {
|
||||
const node = this.parent.nodes.find((n) => n.id === error.nodeId);
|
||||
if (node) {
|
||||
nodeName = node.name || `${node.type}节点`;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeName) {
|
||||
return `${nodeName}: ${error.message}`
|
||||
}
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nodeName) {
|
||||
return `${nodeName}: ${error.message}`;
|
||||
}
|
||||
return error.message;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.check-list-panel {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
width: 400px;
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
span:first-child {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
span:first-child {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
padding: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.check-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.check-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
&.success {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
}
|
||||
&.success {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
}
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #ef4444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #ef4444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
&.warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #22c55e;
|
||||
}
|
||||
&.success {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-type {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #f59e0b;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #f59e0b;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.success {
|
||||
color: #22c55e;
|
||||
}
|
||||
&.success {
|
||||
color: #22c55e;
|
||||
}
|
||||
}
|
||||
|
||||
.item-message {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
line-height: 1.4;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
padding: 4px;
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -13,16 +13,13 @@
|
||||
<div class="p-4 flex flex-col h-[calc(100vh-150px)]">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="text-base font-medium text-gray-700">AI对话</h3>
|
||||
|
||||
|
||||
<div class="flex">
|
||||
<!-- 流式调用开关 -->
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-600 mr-2">流式:</span>
|
||||
<el-switch
|
||||
v-model="chat.isStream"
|
||||
class="mr-4"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-600 mr-2">流式:</span>
|
||||
<el-switch v-model="chat.isStream" class="mr-4" />
|
||||
</div>
|
||||
<el-button link type="primary" class="flex text-gray-500 hover:text-blue-500" @click="chat.showParams = !chat.showParams">
|
||||
<i class="mr-1 text-lg i-tabler-settings"></i>
|
||||
<span class="text-sm">{{ chat.showParams ? '隐藏设置' : '显示设置' }}</span>
|
||||
@@ -384,12 +381,15 @@ const scrollToBottom = (smooth = true) => {
|
||||
}
|
||||
|
||||
// 确保滚动生效的备用方案
|
||||
setTimeout(() => {
|
||||
if (wrapper.scrollTop + wrapper.clientHeight < scrollHeight - 10) {
|
||||
chatScrollbarRef.value.setScrollTop(scrollHeight);
|
||||
}
|
||||
isAtBottom.value = true;
|
||||
}, smooth ? 100 : 10);
|
||||
setTimeout(
|
||||
() => {
|
||||
if (wrapper.scrollTop + wrapper.clientHeight < scrollHeight - 10) {
|
||||
chatScrollbarRef.value.setScrollTop(scrollHeight);
|
||||
}
|
||||
isAtBottom.value = true;
|
||||
},
|
||||
smooth ? 100 : 10
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
// 静默处理错误
|
||||
@@ -400,19 +400,19 @@ const scrollToBottom = (smooth = true) => {
|
||||
// 专门用于流式传输的滚动函数(带防抖)
|
||||
const scrollToBottomForStreaming = () => {
|
||||
if (!chatScrollbarRef.value || streamingMessageIndex.value === -1) return;
|
||||
|
||||
|
||||
// 清除之前的定时器
|
||||
if (scrollDebounceTimer) {
|
||||
clearTimeout(scrollDebounceTimer);
|
||||
}
|
||||
|
||||
|
||||
// 设置防抖,避免过度频繁的滚动
|
||||
scrollDebounceTimer = setTimeout(() => {
|
||||
nextTick(() => {
|
||||
try {
|
||||
const wrapper = chatScrollbarRef.value.wrapRef;
|
||||
if (!wrapper) return;
|
||||
|
||||
|
||||
// 检查是否需要滚动(只有在用户在底部时才自动滚动)
|
||||
if (isAtBottom.value) {
|
||||
const scrollHeight = wrapper.scrollHeight;
|
||||
@@ -471,13 +471,13 @@ const handleClear = async () => {
|
||||
isExecuting.value = false;
|
||||
streamingMessageIndex.value = -1;
|
||||
currentStreamContent.value = '';
|
||||
|
||||
|
||||
// 清理防抖定时器
|
||||
if (scrollDebounceTimer) {
|
||||
clearTimeout(scrollDebounceTimer);
|
||||
scrollDebounceTimer = null;
|
||||
}
|
||||
|
||||
|
||||
ElMessage.success('聊天记录已清空');
|
||||
};
|
||||
|
||||
@@ -536,7 +536,7 @@ const executionStatus = computed(() => {
|
||||
if (props.finalResult?.result?.nodes && props.finalResult.result.nodes.length > 0) {
|
||||
const nodes = props.finalResult.result.nodes;
|
||||
const lastNode = nodes[nodes.length - 1];
|
||||
|
||||
|
||||
if (lastNode && lastNode.status) {
|
||||
const statusMap = {
|
||||
running: { text: '运行中', class: 'status-running' },
|
||||
@@ -547,7 +547,7 @@ const executionStatus = computed(() => {
|
||||
return statusMap[lastNode.status] || { text: '等待中', class: 'status-pending' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有 finalResult 的 nodes,则使用 executionNodes
|
||||
const lastNode = props.executionNodes[props.executionNodes.length - 1];
|
||||
|
||||
@@ -685,7 +685,7 @@ const sendMessage = () => {
|
||||
content: '',
|
||||
isStreaming: true,
|
||||
});
|
||||
|
||||
|
||||
streamingMessageIndex.value = aiMessageIndex;
|
||||
currentStreamContent.value = '';
|
||||
// 由于已经有流式消息占位符,不需要额外的 loading 状态
|
||||
@@ -695,7 +695,7 @@ const sendMessage = () => {
|
||||
isLoading.value = true;
|
||||
streamingMessageIndex.value = -1;
|
||||
}
|
||||
|
||||
|
||||
userInput.value = '';
|
||||
|
||||
// Force scroll to bottom after sending message
|
||||
@@ -717,7 +717,7 @@ watch(
|
||||
if (currentMessage && currentMessage.role === 'ai') {
|
||||
// 更新流式消息内容
|
||||
currentMessage.content = newResult.chatMessage;
|
||||
|
||||
|
||||
// 检查是否完成流式传输
|
||||
if (!newResult.isStreaming) {
|
||||
currentMessage.isStreaming = false;
|
||||
@@ -725,7 +725,7 @@ watch(
|
||||
isLoading.value = false;
|
||||
isExecuting.value = false;
|
||||
}
|
||||
|
||||
|
||||
// 滚动到底部(流式传输时使用专门的滚动函数)
|
||||
if (newResult.isStreaming) {
|
||||
scrollToBottomForStreaming();
|
||||
@@ -742,7 +742,7 @@ watch(
|
||||
// 如果没有流式消息,但有结果,显示传统格式
|
||||
if (newResult.result) {
|
||||
let responseText = '';
|
||||
|
||||
|
||||
// 如果有聊天消息字段,优先显示
|
||||
if (newResult.chatMessage) {
|
||||
responseText = newResult.chatMessage;
|
||||
@@ -1049,8 +1049,6 @@ onUpdated(() => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
80%,
|
||||
|
||||
@@ -1,196 +1,166 @@
|
||||
<template>
|
||||
<!-- 开场白和开场问题编辑器 -->
|
||||
<el-dialog v-model="dialogVisible" title="提示词" width="600px" :show-footer="false">
|
||||
<div class="greeting-editor">
|
||||
<!-- 开场白编辑区 -->
|
||||
<div class="greeting-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-help">
|
||||
<span>聊天开场白</span>
|
||||
<el-tooltip content="开场白会在用户进入对话时首先展示,用于介绍AI助手的功能和特点。" placement="top">
|
||||
<el-icon class="help-icon">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="form.greeting" type="textarea" :rows="5" class="greeting-input" placeholder="在这里编写AI助手的开场白" />
|
||||
</div>
|
||||
|
||||
<!-- 开场白和开场问题编辑器 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="提示词"
|
||||
width="600px"
|
||||
:show-footer="false"
|
||||
>
|
||||
<div class="greeting-editor">
|
||||
<!-- 开场白编辑区 -->
|
||||
<div class="greeting-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-help">
|
||||
<span>聊天开场白</span>
|
||||
<el-tooltip content="开场白会在用户进入对话时首先展示,用于介绍AI助手的功能和特点。"
|
||||
placement="top">
|
||||
<el-icon class="help-icon">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="form.greeting"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
class="greeting-input"
|
||||
placeholder="在这里编写AI助手的开场白"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 开场问题编辑区 -->
|
||||
<div class="questions-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-help">
|
||||
<span>开场问题</span>
|
||||
<div class="question-count">{{ form.questions.length }}/10</div>
|
||||
<el-tooltip content="设置常见问题示例,帮助用户快速开始对话。最多可设置10个问题。"
|
||||
placement="top">
|
||||
<el-icon class="help-icon">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="add-question"
|
||||
@click="addQuestion"
|
||||
:disabled="form.questions.length >= 10"
|
||||
size="small"
|
||||
>
|
||||
<el-icon class="icon-plus">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="questions-list">
|
||||
<div v-for="(question, index) in form.questions"
|
||||
:key="index"
|
||||
class="question-item">
|
||||
<el-input
|
||||
v-model="question.text"
|
||||
class="question-input"
|
||||
:placeholder="'问题 ' + (index + 1)"
|
||||
/>
|
||||
<el-button
|
||||
@click="removeQuestion(index)"
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 开场问题编辑区 -->
|
||||
<div class="questions-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-help">
|
||||
<span>开场问题</span>
|
||||
<div class="question-count">{{ form.questions.length }}/10</div>
|
||||
<el-tooltip content="设置常见问题示例,帮助用户快速开始对话。最多可设置10个问题。" placement="top">
|
||||
<el-icon class="help-icon">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-button type="primary" class="add-question" @click="addQuestion" :disabled="form.questions.length >= 10" size="small">
|
||||
<el-icon class="icon-plus">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="questions-list">
|
||||
<div v-for="(question, index) in form.questions" :key="index" class="question-item">
|
||||
<el-input v-model="question.text" class="question-input" :placeholder="'问题 ' + (index + 1)" />
|
||||
<el-button @click="removeQuestion(index)" type="danger" :icon="Delete" circle size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { QuestionFilled, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import { QuestionFilled, Plus, Delete } from '@element-plus/icons-vue';
|
||||
|
||||
// 注入parent
|
||||
const parent = inject('parent')
|
||||
const parent = inject('parent');
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 对话框显示状态
|
||||
const dialogVisible = ref(props.modelValue)
|
||||
|
||||
const dialogVisible = ref(props.modelValue);
|
||||
|
||||
const form = ref({
|
||||
questions: []
|
||||
})
|
||||
questions: [],
|
||||
});
|
||||
|
||||
// 监听modelValue的变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
form.value = parent.dsl;
|
||||
form.value.questions=form.value.questions || []
|
||||
dialogVisible.value = newVal
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
form.value = parent.dsl;
|
||||
form.value.questions = form.value.questions || [];
|
||||
dialogVisible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听dialogVisible的变化
|
||||
watch(() => dialogVisible.value, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => dialogVisible.value,
|
||||
(newVal) => {
|
||||
emit('update:modelValue', newVal);
|
||||
}
|
||||
);
|
||||
|
||||
// 添加问题
|
||||
const addQuestion = () => {
|
||||
if (form.value.questions.length < 10) {
|
||||
form.value.questions.push({ text: '' })
|
||||
}
|
||||
}
|
||||
if (form.value.questions.length < 10) {
|
||||
form.value.questions.push({ text: '' });
|
||||
}
|
||||
};
|
||||
|
||||
// 删除问题
|
||||
const removeQuestion = (index) => {
|
||||
form.value.questions.splice(index, 1)
|
||||
}
|
||||
|
||||
form.value.questions.splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.greeting-editor {
|
||||
.greeting-section,
|
||||
.questions-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.greeting-section,
|
||||
.questions-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.title-with-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.title-with-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.help-icon {
|
||||
font-size: 16px;
|
||||
color: #909399;
|
||||
cursor: help;
|
||||
}
|
||||
.help-icon {
|
||||
font-size: 16px;
|
||||
color: #909399;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.question-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.question-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.greeting-input {
|
||||
:deep(.el-textarea__inner) {
|
||||
min-height: 60px;
|
||||
max-height: 120px;
|
||||
}
|
||||
}
|
||||
.greeting-input {
|
||||
:deep(.el-textarea__inner) {
|
||||
min-height: 60px;
|
||||
max-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.questions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
.questions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.question-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.question-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.question-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.question-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,174 +1,164 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="数据结构"
|
||||
v-model="dialogVisible"
|
||||
width="800px"
|
||||
class="json-preview-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<div class="preview-content">
|
||||
<!-- 顶部标签页 -->
|
||||
<el-tabs v-model="activeTab" class="preview-tabs">
|
||||
<el-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:label="tab.label"
|
||||
:name="tab.key"
|
||||
>
|
||||
<template #label>
|
||||
<span>{{ tab.label }}</span>
|
||||
<el-tag v-if="tab.key === 'nodes'" size="small" type="info" class="ml-2">
|
||||
{{ nodeCount }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-dialog
|
||||
title="数据结构"
|
||||
v-model="dialogVisible"
|
||||
width="800px"
|
||||
class="json-preview-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<div class="preview-content">
|
||||
<!-- 顶部标签页 -->
|
||||
<el-tabs v-model="activeTab" class="preview-tabs">
|
||||
<el-tab-pane v-for="tab in tabs" :key="tab.key" :label="tab.label" :name="tab.key">
|
||||
<template #label>
|
||||
<span>{{ tab.label }}</span>
|
||||
<el-tag v-if="tab.key === 'nodes'" size="small" type="info" class="ml-2">
|
||||
{{ nodeCount }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 代码编辑器 -->
|
||||
<div class="preview-body">
|
||||
<code-editor
|
||||
v-model="currentTabData"
|
||||
:json="true"
|
||||
:readonly="false"
|
||||
theme="nord"
|
||||
height="400px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="copyData">
|
||||
<el-icon><Document /></el-icon>
|
||||
复制内容
|
||||
</el-button>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 代码编辑器 -->
|
||||
<div class="preview-body">
|
||||
<code-editor v-model="currentTabData" :json="true" :readonly="false" theme="nord" height="400px" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="copyData">
|
||||
<el-icon><Document /></el-icon>
|
||||
复制内容
|
||||
</el-button>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import CodeEditor from "/@/views/knowledge/aiFlow/components/CodeEditor.vue";
|
||||
import { Document } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import CodeEditor from '/@/views/knowledge/aiFlow/components/CodeEditor.vue';
|
||||
|
||||
export default {
|
||||
name: 'JsonPreviewPanel',
|
||||
components: {
|
||||
CodeEditor,
|
||||
Document
|
||||
},
|
||||
inject: ['parent'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'all',
|
||||
tabs: [
|
||||
{ key: 'all', label: '全部数据' },
|
||||
{ key: 'nodes', label: '节点数据' },
|
||||
{ key: 'connections', label: '连线数据' },
|
||||
{ key: 'execution', label: '执行顺序' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodes: this.parent.nodes,
|
||||
connections: this.parent.connections,
|
||||
executionOrder: this.parent.workflowExecutionOrder,
|
||||
}
|
||||
},
|
||||
nodeCount() {
|
||||
return this.data.nodes ? this.data.nodes.length : 0
|
||||
},
|
||||
currentTabData: {
|
||||
get() {
|
||||
let data = ''
|
||||
switch (this.activeTab) {
|
||||
case 'nodes':
|
||||
data = this.data.nodes
|
||||
break
|
||||
case 'connections':
|
||||
data = this.data.connections
|
||||
break
|
||||
case 'execution':
|
||||
data = this.data.executionOrder
|
||||
break
|
||||
default:
|
||||
data = this.data
|
||||
}
|
||||
return JSON.stringify(data, null, 2)
|
||||
},
|
||||
set() {
|
||||
// 只读模式,不需要实现set
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyData() {
|
||||
navigator.clipboard.writeText(this.currentTabData)
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
message: '复制成功',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
ElMessage({
|
||||
message: '复制失败',
|
||||
type: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
console.error('复制失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
name: 'JsonPreviewPanel',
|
||||
components: {
|
||||
CodeEditor,
|
||||
Document,
|
||||
},
|
||||
inject: ['parent'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'all',
|
||||
tabs: [
|
||||
{ key: 'all', label: '全部数据' },
|
||||
{ key: 'nodes', label: '节点数据' },
|
||||
{ key: 'connections', label: '连线数据' },
|
||||
{ key: 'execution', label: '执行顺序' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodes: this.parent.nodes,
|
||||
connections: this.parent.connections,
|
||||
executionOrder: this.parent.workflowExecutionOrder,
|
||||
};
|
||||
},
|
||||
nodeCount() {
|
||||
return this.data.nodes ? this.data.nodes.length : 0;
|
||||
},
|
||||
currentTabData: {
|
||||
get() {
|
||||
let data = '';
|
||||
switch (this.activeTab) {
|
||||
case 'nodes':
|
||||
data = this.data.nodes;
|
||||
break;
|
||||
case 'connections':
|
||||
data = this.data.connections;
|
||||
break;
|
||||
case 'execution':
|
||||
data = this.data.executionOrder;
|
||||
break;
|
||||
default:
|
||||
data = this.data;
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
},
|
||||
set() {
|
||||
// 只读模式,不需要实现set
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyData() {
|
||||
navigator.clipboard
|
||||
.writeText(this.currentTabData)
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
message: '复制成功',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage({
|
||||
message: '复制失败',
|
||||
type: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.json-preview-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
}
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
.preview-tabs {
|
||||
padding: 0 15px;
|
||||
}
|
||||
.preview-content {
|
||||
.preview-tabs {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
.preview-body {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
.dialog-footer {
|
||||
padding: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
:deep(.el-tag) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,329 +1,327 @@
|
||||
<!-- 缩略图组件 -->
|
||||
<template>
|
||||
<div class="mini-map" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<!-- 缩略图容器 -->
|
||||
<div class="mini-map-container" ref="container">
|
||||
<!-- 缩略图内容 -->
|
||||
<div class="mini-map-content"
|
||||
:style="{ transform: `translate(${contentPosition.x}px, ${contentPosition.y}px) scale(${scale})` }">
|
||||
<!-- 节点缩略图 -->
|
||||
<div v-for="node in nodes"
|
||||
:key="node.id"
|
||||
class="mini-node"
|
||||
:style="{
|
||||
left: `${node.x}px`,
|
||||
top: `${node.y}px`,
|
||||
backgroundColor: getNodeColor(node.type)
|
||||
}">
|
||||
</div>
|
||||
<!-- 连线缩略图 -->
|
||||
<svg class="mini-connections" :style="{ width: `${bounds.width}px`, height: `${bounds.height}px` }">
|
||||
<path v-for="(conn, index) in connections"
|
||||
:key="index"
|
||||
:d="getConnectionPath(conn)"
|
||||
class="mini-connection-path"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 视口指示器 -->
|
||||
<div class="viewport-indicator"
|
||||
:style="{
|
||||
transform: `translate(${viewportPosition.x}px, ${viewportPosition.y}px)`,
|
||||
width: `${viewportSize.width}px`,
|
||||
height: `${viewportSize.height}px`
|
||||
}"
|
||||
@mousedown.prevent="startDrag">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-map" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<!-- 缩略图容器 -->
|
||||
<div class="mini-map-container" ref="container">
|
||||
<!-- 缩略图内容 -->
|
||||
<div class="mini-map-content" :style="{ transform: `translate(${contentPosition.x}px, ${contentPosition.y}px) scale(${scale})` }">
|
||||
<!-- 节点缩略图 -->
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="node.id"
|
||||
class="mini-node"
|
||||
:style="{
|
||||
left: `${node.x}px`,
|
||||
top: `${node.y}px`,
|
||||
backgroundColor: getNodeColor(node.type),
|
||||
}"
|
||||
></div>
|
||||
<!-- 连线缩略图 -->
|
||||
<svg class="mini-connections" :style="{ width: `${bounds.width}px`, height: `${bounds.height}px` }">
|
||||
<path v-for="(conn, index) in connections" :key="index" :d="getConnectionPath(conn)" class="mini-connection-path" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 视口指示器 -->
|
||||
<div
|
||||
class="viewport-indicator"
|
||||
:style="{
|
||||
transform: `translate(${viewportPosition.x}px, ${viewportPosition.y}px)`,
|
||||
width: `${viewportSize.width}px`,
|
||||
height: `${viewportSize.height}px`,
|
||||
}"
|
||||
@mousedown.prevent="startDrag"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MiniMap',
|
||||
props: {
|
||||
// 缩略图宽度
|
||||
width: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
// 缩略图高度
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
// 节点数据
|
||||
nodes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 连接数据
|
||||
connections: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 画布缩放比例
|
||||
zoom: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 画布位置
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 })
|
||||
},
|
||||
// 添加容器尺寸属性
|
||||
containerSize: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scale: 0.1,
|
||||
isDragging: false,
|
||||
dragStart: { x: 0, y: 0 },
|
||||
bounds: {
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
width: 3000,
|
||||
height: 3000
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 计算内容位置,使其居中显示
|
||||
contentPosition() {
|
||||
const offsetX = (this.width - this.bounds.width * this.scale) / 2
|
||||
const offsetY = (this.height - this.bounds.height * this.scale) / 2
|
||||
return {
|
||||
x: offsetX,
|
||||
y: offsetY
|
||||
}
|
||||
},
|
||||
// 修改视口位置计算
|
||||
viewportPosition() {
|
||||
// 确保位置不会超出边界
|
||||
const maxX = this.width - this.viewportSize.width
|
||||
const maxY = this.height - this.viewportSize.height
|
||||
|
||||
let x = (-this.position.x * this.scale) + this.contentPosition.x
|
||||
let y = (-this.position.y * this.scale) + this.contentPosition.y
|
||||
|
||||
// 限制在有效范围内
|
||||
x = Math.max(0, Math.min(x, maxX))
|
||||
y = Math.max(0, Math.min(y, maxY))
|
||||
|
||||
return { x, y }
|
||||
},
|
||||
// 修改视口尺寸计算
|
||||
viewportSize() {
|
||||
// 计算缩略图内容的实际显示范围
|
||||
const contentWidth = this.bounds.width * this.scale
|
||||
const contentHeight = this.bounds.height * this.scale
|
||||
|
||||
// 计算视口尺寸比例
|
||||
const viewportRatioX = this.width / (this.bounds.width / this.zoom)
|
||||
const viewportRatioY = this.height / (this.bounds.height / this.zoom)
|
||||
|
||||
// 确保视口尺寸不会小于最小值或大于缩略图尺寸
|
||||
return {
|
||||
width: Math.min(this.width, Math.max(50, contentWidth * viewportRatioX)),
|
||||
height: Math.min(this.height, Math.max(30, contentHeight * viewportRatioY))
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听节点变化,重新计算边界和缩放
|
||||
nodes: {
|
||||
handler() {
|
||||
this.$nextTick(this.updateBoundsAndScale)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取节点颜色
|
||||
getNodeColor(type) {
|
||||
const colors = {
|
||||
start: '#10B981',
|
||||
end: '#EF4444',
|
||||
http: '#3B82F6',
|
||||
switch: '#F59E0B',
|
||||
code: '#8B5CF6',
|
||||
db: '#6366F1',
|
||||
llm: '#EC4899',
|
||||
notice: '#14B8A6',
|
||||
question: '#F97316',
|
||||
default: '#6B7280'
|
||||
}
|
||||
return colors[type] || colors.default
|
||||
},
|
||||
// 获取连接路径
|
||||
getConnectionPath(conn) {
|
||||
const source = this.nodes.find(n => n.id === conn.sourceId)
|
||||
const target = this.nodes.find(n => n.id === conn.targetId)
|
||||
if (!source || !target) return ''
|
||||
name: 'MiniMap',
|
||||
props: {
|
||||
// 缩略图宽度
|
||||
width: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
// 缩略图高度
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
// 节点数据
|
||||
nodes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 连接数据
|
||||
connections: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 画布缩放比例
|
||||
zoom: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
// 画布位置
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
// 添加容器尺寸属性
|
||||
containerSize: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scale: 0.1,
|
||||
isDragging: false,
|
||||
dragStart: { x: 0, y: 0 },
|
||||
bounds: {
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
width: 3000,
|
||||
height: 3000,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 计算内容位置,使其居中显示
|
||||
contentPosition() {
|
||||
const offsetX = (this.width - this.bounds.width * this.scale) / 2;
|
||||
const offsetY = (this.height - this.bounds.height * this.scale) / 2;
|
||||
return {
|
||||
x: offsetX,
|
||||
y: offsetY,
|
||||
};
|
||||
},
|
||||
// 修改视口位置计算
|
||||
viewportPosition() {
|
||||
// 确保位置不会超出边界
|
||||
const maxX = this.width - this.viewportSize.width;
|
||||
const maxY = this.height - this.viewportSize.height;
|
||||
|
||||
const x1 = source.x
|
||||
const y1 = source.y
|
||||
const x2 = target.x
|
||||
const y2 = target.y
|
||||
let x = -this.position.x * this.scale + this.contentPosition.x;
|
||||
let y = -this.position.y * this.scale + this.contentPosition.y;
|
||||
|
||||
// 计算控制点
|
||||
const dx = Math.abs(x2 - x1) * 0.5
|
||||
const cp1x = x1 + dx
|
||||
const cp1y = y1
|
||||
const cp2x = x2 - dx
|
||||
const cp2y = y2
|
||||
// 限制在有效范围内
|
||||
x = Math.max(0, Math.min(x, maxX));
|
||||
y = Math.max(0, Math.min(y, maxY));
|
||||
|
||||
return `M ${x1} ${y1} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`
|
||||
},
|
||||
// 开始拖动视口
|
||||
startDrag(event) {
|
||||
this.isDragging = true
|
||||
const rect = this.$refs.container.getBoundingClientRect()
|
||||
this.dragStart = {
|
||||
x: event.clientX - rect.left - this.viewportPosition.x,
|
||||
y: event.clientY - rect.top - this.viewportPosition.y
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', this.onDrag)
|
||||
window.addEventListener('mouseup', this.stopDrag)
|
||||
},
|
||||
// 拖动中
|
||||
onDrag(event) {
|
||||
if (!this.isDragging) return
|
||||
|
||||
const rect = this.$refs.container.getBoundingClientRect()
|
||||
let x = event.clientX - rect.left - this.dragStart.x
|
||||
let y = event.clientY - rect.top - this.dragStart.y
|
||||
|
||||
// 添加边界限制
|
||||
const maxX = this.width - this.viewportSize.width
|
||||
const maxY = this.height - this.viewportSize.height
|
||||
x = Math.max(0, Math.min(x, maxX))
|
||||
y = Math.max(0, Math.min(y, maxY))
|
||||
|
||||
// 计算相对于内容的位置
|
||||
const relativeX = (x - this.contentPosition.x) / this.scale
|
||||
const relativeY = (y - this.contentPosition.y) / this.scale
|
||||
|
||||
this.$emit('update:position', {
|
||||
x: -relativeX,
|
||||
y: -relativeY
|
||||
})
|
||||
},
|
||||
// 停止拖动
|
||||
stopDrag() {
|
||||
this.isDragging = false
|
||||
window.removeEventListener('mousemove', this.onDrag)
|
||||
window.removeEventListener('mouseup', this.stopDrag)
|
||||
},
|
||||
// 更新边界和缩放
|
||||
updateBoundsAndScale() {
|
||||
if (!this.nodes.length) return
|
||||
return { x, y };
|
||||
},
|
||||
// 修改视口尺寸计算
|
||||
viewportSize() {
|
||||
// 计算缩略图内容的实际显示范围
|
||||
const contentWidth = this.bounds.width * this.scale;
|
||||
const contentHeight = this.bounds.height * this.scale;
|
||||
|
||||
// 计算节点边界
|
||||
const nodePositions = this.nodes.map(node => ({
|
||||
left: node.x - 100, // 考虑节点宽度
|
||||
right: node.x + 100,
|
||||
top: node.y - 50, // 考虑节点高度
|
||||
bottom: node.y + 50
|
||||
}))
|
||||
// 计算视口尺寸比例
|
||||
const viewportRatioX = this.width / (this.bounds.width / this.zoom);
|
||||
const viewportRatioY = this.height / (this.bounds.height / this.zoom);
|
||||
|
||||
// 计算整体边界
|
||||
const minX = Math.min(...nodePositions.map(p => p.left))
|
||||
const maxX = Math.max(...nodePositions.map(p => p.right))
|
||||
const minY = Math.min(...nodePositions.map(p => p.top))
|
||||
const maxY = Math.max(...nodePositions.map(p => p.bottom))
|
||||
// 确保视口尺寸不会小于最小值或大于缩略图尺寸
|
||||
return {
|
||||
width: Math.min(this.width, Math.max(50, contentWidth * viewportRatioX)),
|
||||
height: Math.min(this.height, Math.max(30, contentHeight * viewportRatioY)),
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// 监听节点变化,重新计算边界和缩放
|
||||
nodes: {
|
||||
handler() {
|
||||
this.$nextTick(this.updateBoundsAndScale);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 获取节点颜色
|
||||
getNodeColor(type) {
|
||||
const colors = {
|
||||
start: '#10B981',
|
||||
end: '#EF4444',
|
||||
http: '#3B82F6',
|
||||
switch: '#F59E0B',
|
||||
code: '#8B5CF6',
|
||||
db: '#6366F1',
|
||||
llm: '#EC4899',
|
||||
notice: '#14B8A6',
|
||||
question: '#F97316',
|
||||
default: '#6B7280',
|
||||
};
|
||||
return colors[type] || colors.default;
|
||||
},
|
||||
// 获取连接路径
|
||||
getConnectionPath(conn) {
|
||||
const source = this.nodes.find((n) => n.id === conn.sourceId);
|
||||
const target = this.nodes.find((n) => n.id === conn.targetId);
|
||||
if (!source || !target) return '';
|
||||
|
||||
// 添加边距
|
||||
const PADDING = 100
|
||||
this.bounds = {
|
||||
minX: minX - PADDING,
|
||||
minY: minY - PADDING,
|
||||
width: maxX - minX + PADDING * 2,
|
||||
height: maxY - minY + PADDING * 2
|
||||
}
|
||||
const x1 = source.x;
|
||||
const y1 = source.y;
|
||||
const x2 = target.x;
|
||||
const y2 = target.y;
|
||||
|
||||
// 计算合适的缩放比例
|
||||
const scaleX = this.width / this.bounds.width
|
||||
const scaleY = this.height / this.bounds.height
|
||||
this.scale = Math.min(scaleX, scaleY, 0.2) // 限制最大缩放比例为0.2
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateBoundsAndScale()
|
||||
}
|
||||
}
|
||||
// 计算控制点
|
||||
const dx = Math.abs(x2 - x1) * 0.5;
|
||||
const cp1x = x1 + dx;
|
||||
const cp1y = y1;
|
||||
const cp2x = x2 - dx;
|
||||
const cp2y = y2;
|
||||
|
||||
return `M ${x1} ${y1} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`;
|
||||
},
|
||||
// 开始拖动视口
|
||||
startDrag(event) {
|
||||
this.isDragging = true;
|
||||
const rect = this.$refs.container.getBoundingClientRect();
|
||||
this.dragStart = {
|
||||
x: event.clientX - rect.left - this.viewportPosition.x,
|
||||
y: event.clientY - rect.top - this.viewportPosition.y,
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', this.onDrag);
|
||||
window.addEventListener('mouseup', this.stopDrag);
|
||||
},
|
||||
// 拖动中
|
||||
onDrag(event) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const rect = this.$refs.container.getBoundingClientRect();
|
||||
let x = event.clientX - rect.left - this.dragStart.x;
|
||||
let y = event.clientY - rect.top - this.dragStart.y;
|
||||
|
||||
// 添加边界限制
|
||||
const maxX = this.width - this.viewportSize.width;
|
||||
const maxY = this.height - this.viewportSize.height;
|
||||
x = Math.max(0, Math.min(x, maxX));
|
||||
y = Math.max(0, Math.min(y, maxY));
|
||||
|
||||
// 计算相对于内容的位置
|
||||
const relativeX = (x - this.contentPosition.x) / this.scale;
|
||||
const relativeY = (y - this.contentPosition.y) / this.scale;
|
||||
|
||||
this.$emit('update:position', {
|
||||
x: -relativeX,
|
||||
y: -relativeY,
|
||||
});
|
||||
},
|
||||
// 停止拖动
|
||||
stopDrag() {
|
||||
this.isDragging = false;
|
||||
window.removeEventListener('mousemove', this.onDrag);
|
||||
window.removeEventListener('mouseup', this.stopDrag);
|
||||
},
|
||||
// 更新边界和缩放
|
||||
updateBoundsAndScale() {
|
||||
if (!this.nodes.length) return;
|
||||
|
||||
// 计算节点边界
|
||||
const nodePositions = this.nodes.map((node) => ({
|
||||
left: node.x - 100, // 考虑节点宽度
|
||||
right: node.x + 100,
|
||||
top: node.y - 50, // 考虑节点高度
|
||||
bottom: node.y + 50,
|
||||
}));
|
||||
|
||||
// 计算整体边界
|
||||
const minX = Math.min(...nodePositions.map((p) => p.left));
|
||||
const maxX = Math.max(...nodePositions.map((p) => p.right));
|
||||
const minY = Math.min(...nodePositions.map((p) => p.top));
|
||||
const maxY = Math.max(...nodePositions.map((p) => p.bottom));
|
||||
|
||||
// 添加边距
|
||||
const PADDING = 100;
|
||||
this.bounds = {
|
||||
minX: minX - PADDING,
|
||||
minY: minY - PADDING,
|
||||
width: maxX - minX + PADDING * 2,
|
||||
height: maxY - minY + PADDING * 2,
|
||||
};
|
||||
|
||||
// 计算合适的缩放比例
|
||||
const scaleX = this.width / this.bounds.width;
|
||||
const scaleY = this.height / this.bounds.height;
|
||||
this.scale = Math.min(scaleX, scaleY, 0.2); // 限制最大缩放比例为0.2
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateBoundsAndScale();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mini-map {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 60px;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.mini-map-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-map-content {
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.mini-node {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.mini-connections {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.mini-connection-path {
|
||||
fill: none;
|
||||
stroke: #94a3b8;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.viewport-indicator {
|
||||
position: absolute;
|
||||
border: 1px solid #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.05); // 降低默认透明度,提高对比度
|
||||
pointer-events: all;
|
||||
cursor: move;
|
||||
border-radius: 4px; // 添加圆角
|
||||
transition: all 0.2s ease; // 添加过渡动画
|
||||
|
||||
&:hover {
|
||||
background: rgba(59, 130, 246, 0.15); // 提高hover时的透明度
|
||||
border-color: #2563eb; // hover时加深边框颜色
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); // hover时加深阴影
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(59, 130, 246, 0.2); // 点击时加深背景色
|
||||
border-color: #1d4ed8; // 点击时进一步加深边框
|
||||
}
|
||||
}
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 60px;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.mini-map-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-map-content {
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.mini-node {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.mini-connections {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.mini-connection-path {
|
||||
fill: none;
|
||||
stroke: #94a3b8;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.viewport-indicator {
|
||||
position: absolute;
|
||||
border: 1px solid #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.05); // 降低默认透明度,提高对比度
|
||||
pointer-events: all;
|
||||
cursor: move;
|
||||
border-radius: 4px; // 添加圆角
|
||||
transition: all 0.2s ease; // 添加过渡动画
|
||||
|
||||
&:hover {
|
||||
background: rgba(59, 130, 246, 0.15); // 提高hover时的透明度
|
||||
border-color: #2563eb; // hover时加深边框颜色
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); // hover时加深阴影
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(59, 130, 246, 0.2); // 点击时加深背景色
|
||||
border-color: #1d4ed8; // 点击时进一步加深边框
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -60,15 +60,15 @@ export default {
|
||||
if (this.addPosition !== 'right' || !this.node || !this.parent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 获取当前节点已有的连接数量
|
||||
const existingConnections = this.parent.connections?.filter((conn) => conn.sourceId === this.node.id) || [];
|
||||
|
||||
|
||||
// 如果不是分支节点且已经有连接,则不能向右添加
|
||||
if (!['switch', 'question'].includes(this.node.type) && existingConnections.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
},
|
||||
nodeTypes() {
|
||||
|
||||
@@ -1,313 +1,304 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="visible"
|
||||
class="more-menu"
|
||||
:style="menuStyle">
|
||||
<!-- 拷贝 -->
|
||||
<div class="menu-item"
|
||||
@click="copyNode">
|
||||
<span>复制</span>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="visible" class="more-menu" :style="menuStyle">
|
||||
<!-- 拷贝 -->
|
||||
<div class="menu-item" @click="copyNode">
|
||||
<span>复制</span>
|
||||
</div>
|
||||
|
||||
<!-- 新增子节点 -->
|
||||
<div class="menu-item"
|
||||
@click="handleShowAddChild"
|
||||
v-if="canAddChild">
|
||||
<span>新增子节点</span>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
<!-- 新增子节点 -->
|
||||
<div class="menu-item" @click="handleShowAddChild" v-if="canAddChild">
|
||||
<span>新增子节点</span>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
<!-- 更换类型 -->
|
||||
<div class="menu-item"
|
||||
@click="handleShowChangeType">
|
||||
<span>更换类型</span>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
<!-- 更换类型 -->
|
||||
<div class="menu-item" @click="handleShowChangeType">
|
||||
<span>更换类型</span>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
<!-- 删除 -->
|
||||
<div class="menu-item"
|
||||
@click="deleteNode" v-if="node.type !== 'start'">
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 删除 -->
|
||||
<div class="menu-item" @click="deleteNode" v-if="node.type !== 'start'">
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用 NodeContextMenu 组件 - 更换类型 -->
|
||||
<NodeContextMenu v-model:visible="showChangeType"
|
||||
:position="changeTypeMenuPosition"
|
||||
:node="node"
|
||||
add-position="replace"
|
||||
@add="handleChangeType" />
|
||||
<!-- 使用 NodeContextMenu 组件 - 更换类型 -->
|
||||
<NodeContextMenu
|
||||
v-model:visible="showChangeType"
|
||||
:position="changeTypeMenuPosition"
|
||||
:node="node"
|
||||
add-position="replace"
|
||||
@add="handleChangeType"
|
||||
/>
|
||||
|
||||
<!-- 使用 NodeContextMenu 组件 - 新增子节点 -->
|
||||
<NodeContextMenu v-model:visible="showAddChild"
|
||||
:position="addChildMenuPosition"
|
||||
:node="node"
|
||||
add-position="right"
|
||||
@add="handleAddChild" />
|
||||
</div>
|
||||
<!-- 使用 NodeContextMenu 组件 - 新增子节点 -->
|
||||
<NodeContextMenu v-model:visible="showAddChild" :position="addChildMenuPosition" :node="node" add-position="right" @add="handleAddChild" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NodeContextMenu from './NodeContextMenu.vue'
|
||||
import { getNodeConfig } from './nodes/nodeTypes.ts'
|
||||
import NodeContextMenu from './NodeContextMenu.vue';
|
||||
import { getNodeConfig } from './nodes/nodeTypes.ts';
|
||||
|
||||
export default {
|
||||
name: 'NodeMoreMenu',
|
||||
inject: ['parent'],
|
||||
components: {
|
||||
NodeContextMenu
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 })
|
||||
},
|
||||
node: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
data () {
|
||||
return {
|
||||
showChangeType: false,
|
||||
changeTypeMenuPosition: { x: 0, y: 0 },
|
||||
showAddChild: false,
|
||||
addChildMenuPosition: { x: 0, y: 0 }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
menuStyle () {
|
||||
return {
|
||||
left: `${this.position.x}px`,
|
||||
top: `${this.position.y}px`
|
||||
}
|
||||
},
|
||||
|
||||
// 判断是否可以添加子节点
|
||||
canAddChild () {
|
||||
if (!this.node || !this.parent) return false
|
||||
|
||||
// 结束节点不能添加子节点
|
||||
if (this.node.type === 'end') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取当前节点已有的连接数量
|
||||
const existingConnections = this.parent.connections?.filter((conn) => conn.sourceId === this.node.id) || []
|
||||
|
||||
// 如果是分支节点(switch 或 question),可以添加子节点
|
||||
if (['switch', 'question'].includes(this.node.type)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果不是分支节点且已经有连接,则不能添加子节点
|
||||
if (existingConnections.length > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 复制节点
|
||||
copyNode () {
|
||||
if (!this.node) return
|
||||
name: 'NodeMoreMenu',
|
||||
inject: ['parent'],
|
||||
components: {
|
||||
NodeContextMenu,
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
node: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
data() {
|
||||
return {
|
||||
showChangeType: false,
|
||||
changeTypeMenuPosition: { x: 0, y: 0 },
|
||||
showAddChild: false,
|
||||
addChildMenuPosition: { x: 0, y: 0 },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
menuStyle() {
|
||||
return {
|
||||
left: `${this.position.x}px`,
|
||||
top: `${this.position.y}px`,
|
||||
};
|
||||
},
|
||||
|
||||
// 创建节点的深拷贝
|
||||
const nodeCopy = JSON.parse(JSON.stringify(this.node))
|
||||
// 判断是否可以添加子节点
|
||||
canAddChild() {
|
||||
if (!this.node || !this.parent) return false;
|
||||
|
||||
// 生成新的唯一ID
|
||||
nodeCopy.id = `node_${Date.now()}`
|
||||
// 结束节点不能添加子节点
|
||||
if (this.node.type === 'end') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置新节点的位置(在原节点右下方20px处)
|
||||
nodeCopy.x = this.node.x + 20
|
||||
nodeCopy.y = this.node.y + 20
|
||||
// 获取当前节点已有的连接数量
|
||||
const existingConnections = this.parent.connections?.filter((conn) => conn.sourceId === this.node.id) || [];
|
||||
|
||||
// 更新节点列表
|
||||
this.parent.nodes = [...this.parent.nodes, nodeCopy]
|
||||
// 如果是分支节点(switch 或 question),可以添加子节点
|
||||
if (['switch', 'question'].includes(this.node.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
// 如果不是分支节点且已经有连接,则不能添加子节点
|
||||
if (existingConnections.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 删除节点
|
||||
deleteNode () {
|
||||
if (!this.node) return
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 复制节点
|
||||
copyNode() {
|
||||
if (!this.node) return;
|
||||
|
||||
// 删除节点的所有端点
|
||||
this.parent.jsPlumbInstance.removeAllEndpoints(this.node.id)
|
||||
// 创建节点的深拷贝
|
||||
const nodeCopy = JSON.parse(JSON.stringify(this.node));
|
||||
|
||||
// 从节点列表中删除节点
|
||||
this.parent.nodes= this.parent.nodes.filter(n => n.id !== this.node.id)
|
||||
// 生成新的唯一ID
|
||||
nodeCopy.id = `node_${Date.now()}`;
|
||||
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
// 设置新节点的位置(在原节点右下方20px处)
|
||||
nodeCopy.x = this.node.x + 20;
|
||||
nodeCopy.y = this.node.y + 20;
|
||||
|
||||
// 更换节点类型
|
||||
changeNodeType (newType) {
|
||||
if (!this.node || !newType) return
|
||||
// 更新节点列表
|
||||
this.parent.nodes = [...this.parent.nodes, nodeCopy];
|
||||
|
||||
// 获取新节点类型的配置
|
||||
const nodeConfig = getNodeConfig(newType)
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
|
||||
// 找到当前节点的索引
|
||||
const nodeIndex = this.parent.nodes.findIndex(n => n.id === this.node.id)
|
||||
if (nodeIndex === -1) return
|
||||
// 删除节点
|
||||
deleteNode() {
|
||||
if (!this.node) return;
|
||||
|
||||
// 保存原节点的位置和ID
|
||||
const { x, y, id } = this.node
|
||||
// 删除节点的所有端点
|
||||
this.parent.jsPlumbInstance.removeAllEndpoints(this.node.id);
|
||||
|
||||
// 创建新节点,保持原有的位置和ID
|
||||
const newNode = {
|
||||
...nodeConfig,
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
}
|
||||
// 从节点列表中删除节点
|
||||
this.parent.nodes = this.parent.nodes.filter((n) => n.id !== this.node.id);
|
||||
|
||||
// 更新节点列表
|
||||
const newNodes = [...this.parent.nodes]
|
||||
newNodes[nodeIndex] = newNode
|
||||
this.parent.nodes = newNodes
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false)
|
||||
this.showChangeType = false
|
||||
},
|
||||
// 更换节点类型
|
||||
changeNodeType(newType) {
|
||||
if (!this.node || !newType) return;
|
||||
|
||||
handleShowChangeType (event) {
|
||||
// 计算子菜单位置,在父菜单右侧显示
|
||||
const menuEl = this.$el.querySelector('.more-menu')
|
||||
if (menuEl) {
|
||||
const rect = menuEl.getBoundingClientRect()
|
||||
// 检查右侧空间是否足够
|
||||
const rightSpace = window.innerWidth - rect.right
|
||||
const leftSpace = rect.left
|
||||
// 获取新节点类型的配置
|
||||
const nodeConfig = getNodeConfig(newType);
|
||||
|
||||
// 如果右侧空间不足,且左侧空间足够,则显示在左侧
|
||||
if (rightSpace < 200 && leftSpace > 200) {
|
||||
this.changeTypeMenuPosition = {
|
||||
x: rect.left - 5, // 左侧偏移5px
|
||||
y: rect.top
|
||||
}
|
||||
} else {
|
||||
this.changeTypeMenuPosition = {
|
||||
x: rect.right + 5, // 右侧偏移5px
|
||||
y: rect.top
|
||||
}
|
||||
}
|
||||
}
|
||||
this.showChangeType = true
|
||||
// 阻止事件冒泡,防止触发外部点击事件
|
||||
event?.stopPropagation()
|
||||
},
|
||||
// 找到当前节点的索引
|
||||
const nodeIndex = this.parent.nodes.findIndex((n) => n.id === this.node.id);
|
||||
if (nodeIndex === -1) return;
|
||||
|
||||
handleChangeType (type) {
|
||||
this.changeNodeType(type)
|
||||
},
|
||||
// 保存原节点的位置和ID
|
||||
const { x, y, id } = this.node;
|
||||
|
||||
// 显示新增子节点菜单
|
||||
handleShowAddChild (event) {
|
||||
// 计算子菜单位置,在父菜单右侧显示
|
||||
const menuEl = this.$el.querySelector('.more-menu')
|
||||
if (menuEl) {
|
||||
const rect = menuEl.getBoundingClientRect()
|
||||
// 检查右侧空间是否足够
|
||||
const rightSpace = window.innerWidth - rect.right
|
||||
const leftSpace = rect.left
|
||||
// 创建新节点,保持原有的位置和ID
|
||||
const newNode = {
|
||||
...nodeConfig,
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
};
|
||||
|
||||
// 如果右侧空间不足,且左侧空间足够,则显示在左侧
|
||||
if (rightSpace < 200 && leftSpace > 200) {
|
||||
this.addChildMenuPosition = {
|
||||
x: rect.left - 5, // 左侧偏移5px
|
||||
y: rect.top
|
||||
}
|
||||
} else {
|
||||
this.addChildMenuPosition = {
|
||||
x: rect.right + 5, // 右侧偏移5px
|
||||
y: rect.top
|
||||
}
|
||||
}
|
||||
}
|
||||
this.showAddChild = true
|
||||
// 阻止事件冒泡,防止触发外部点击事件
|
||||
event?.stopPropagation()
|
||||
},
|
||||
// 更新节点列表
|
||||
const newNodes = [...this.parent.nodes];
|
||||
newNodes[nodeIndex] = newNode;
|
||||
this.parent.nodes = newNodes;
|
||||
|
||||
// 处理新增子节点
|
||||
handleAddChild (type) {
|
||||
if (!this.node || !this.parent || typeof this.parent.addNode !== 'function') return
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false);
|
||||
this.showChangeType = false;
|
||||
},
|
||||
|
||||
// 模拟右键添加节点的流程
|
||||
this.parent.contextMenuNode = this.node
|
||||
this.parent.contextMenuAddPosition = 'right'
|
||||
this.parent.contextMenuPortIndex = 0
|
||||
handleShowChangeType(event) {
|
||||
// 计算子菜单位置,在父菜单右侧显示
|
||||
const menuEl = this.$el.querySelector('.more-menu');
|
||||
if (menuEl) {
|
||||
const rect = menuEl.getBoundingClientRect();
|
||||
// 检查右侧空间是否足够
|
||||
const rightSpace = window.innerWidth - rect.right;
|
||||
const leftSpace = rect.left;
|
||||
|
||||
// 调用父组件的 addNode 方法
|
||||
this.parent.addNode(type)
|
||||
// 如果右侧空间不足,且左侧空间足够,则显示在左侧
|
||||
if (rightSpace < 200 && leftSpace > 200) {
|
||||
this.changeTypeMenuPosition = {
|
||||
x: rect.left - 5, // 左侧偏移5px
|
||||
y: rect.top,
|
||||
};
|
||||
} else {
|
||||
this.changeTypeMenuPosition = {
|
||||
x: rect.right + 5, // 右侧偏移5px
|
||||
y: rect.top,
|
||||
};
|
||||
}
|
||||
}
|
||||
this.showChangeType = true;
|
||||
// 阻止事件冒泡,防止触发外部点击事件
|
||||
event?.stopPropagation();
|
||||
},
|
||||
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false)
|
||||
this.showAddChild = false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 点击外部关闭菜单
|
||||
const handleClickOutside = (e) => {
|
||||
if (!this.$el.contains(e.target)) {
|
||||
this.$emit('update:visible', false)
|
||||
this.showChangeType = false
|
||||
this.showAddChild = false
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
handleChangeType(type) {
|
||||
this.changeNodeType(type);
|
||||
},
|
||||
|
||||
// 保存引用以便在组件销毁时移除
|
||||
this.handleClickOutside = handleClickOutside
|
||||
},
|
||||
beforeUnmount () {
|
||||
// 移除事件监听器
|
||||
document.removeEventListener('click', this.handleClickOutside)
|
||||
}
|
||||
}
|
||||
// 显示新增子节点菜单
|
||||
handleShowAddChild(event) {
|
||||
// 计算子菜单位置,在父菜单右侧显示
|
||||
const menuEl = this.$el.querySelector('.more-menu');
|
||||
if (menuEl) {
|
||||
const rect = menuEl.getBoundingClientRect();
|
||||
// 检查右侧空间是否足够
|
||||
const rightSpace = window.innerWidth - rect.right;
|
||||
const leftSpace = rect.left;
|
||||
|
||||
// 如果右侧空间不足,且左侧空间足够,则显示在左侧
|
||||
if (rightSpace < 200 && leftSpace > 200) {
|
||||
this.addChildMenuPosition = {
|
||||
x: rect.left - 5, // 左侧偏移5px
|
||||
y: rect.top,
|
||||
};
|
||||
} else {
|
||||
this.addChildMenuPosition = {
|
||||
x: rect.right + 5, // 右侧偏移5px
|
||||
y: rect.top,
|
||||
};
|
||||
}
|
||||
}
|
||||
this.showAddChild = true;
|
||||
// 阻止事件冒泡,防止触发外部点击事件
|
||||
event?.stopPropagation();
|
||||
},
|
||||
|
||||
// 处理新增子节点
|
||||
handleAddChild(type) {
|
||||
if (!this.node || !this.parent || typeof this.parent.addNode !== 'function') return;
|
||||
|
||||
// 模拟右键添加节点的流程
|
||||
this.parent.contextMenuNode = this.node;
|
||||
this.parent.contextMenuAddPosition = 'right';
|
||||
this.parent.contextMenuPortIndex = 0;
|
||||
|
||||
// 调用父组件的 addNode 方法
|
||||
this.parent.addNode(type);
|
||||
|
||||
// 关闭菜单
|
||||
this.$emit('update:visible', false);
|
||||
this.showAddChild = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// 点击外部关闭菜单
|
||||
const handleClickOutside = (e) => {
|
||||
if (!this.$el.contains(e.target)) {
|
||||
this.$emit('update:visible', false);
|
||||
this.showChangeType = false;
|
||||
this.showAddChild = false;
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
// 保存引用以便在组件销毁时移除
|
||||
this.handleClickOutside = handleClickOutside;
|
||||
},
|
||||
beforeUnmount() {
|
||||
// 移除事件监听器
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.more-menu {
|
||||
opacity: 0.9;
|
||||
position: fixed;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
padding: 4px 0;
|
||||
min-width: 160px;
|
||||
z-index: 1000;
|
||||
opacity: 0.9;
|
||||
position: fixed;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
padding: 4px 0;
|
||||
min-width: 160px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: rgb(71 84 103 / 1);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: rgb(71 84 103 / 1);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.el-icon-arrow-right {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,336 +1,329 @@
|
||||
<template>
|
||||
<div class="workflow-designer">
|
||||
<!-- 添加未发布遮罩组件 -->
|
||||
<UnpublishedMask :visible="!form.enabled" />
|
||||
<div class="workflow-designer">
|
||||
<!-- 添加未发布遮罩组件 -->
|
||||
<UnpublishedMask :visible="!form.enabled" />
|
||||
|
||||
<!-- 执行面板 -->
|
||||
<div class="execution-panel"
|
||||
v-if="form.enabled">
|
||||
<div class="panel-header">
|
||||
<h3>流程运行{{ id }}</h3>
|
||||
</div>
|
||||
<!-- 执行面板 -->
|
||||
<div class="execution-panel" v-if="form.enabled">
|
||||
<div class="panel-header">
|
||||
<h3>流程运行{{ id }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<!-- 参数输入区域 -->
|
||||
<div class="left-panel">
|
||||
<div class="variable-inputs">
|
||||
<div v-for="(param, index) in startNodeParams"
|
||||
:key="index"
|
||||
class="input-item">
|
||||
<div class="input-label"
|
||||
:class="{ 'required': param.required }">
|
||||
{{ param.name }}
|
||||
</div>
|
||||
<div class="input-value">
|
||||
<input v-if="param.inputType === 'input'"
|
||||
v-model="param.value"
|
||||
class="param-input"
|
||||
:disabled="isRunning"
|
||||
:class="{ 'error': showError && param.required && !param.value }"
|
||||
:placeholder="'请输入' + param.name" />
|
||||
<input v-else-if="param.inputType === 'number'"
|
||||
type="number"
|
||||
v-model.number="param.value"
|
||||
class="param-input"
|
||||
:disabled="isRunning"
|
||||
:class="{ 'error': showError && param.required && !param.value }"
|
||||
:placeholder="'请输入' + param.name" />
|
||||
<textarea v-else-if="param.inputType === 'textarea'"
|
||||
v-model="param.value"
|
||||
class="param-textarea"
|
||||
:disabled="isRunning"
|
||||
:class="{ 'error': showError && param.required && !param.value }"
|
||||
:placeholder="'请输入' + param.name"
|
||||
rows="3"></textarea>
|
||||
<select v-else-if="param.inputType === 'select'"
|
||||
v-model="param.value"
|
||||
class="param-select"
|
||||
:disabled="isRunning"
|
||||
:class="{ 'error': showError && param.required && !param.value }">
|
||||
<option value="">请选择{{ param.name }}</option>
|
||||
<option v-for="option in param.options"
|
||||
:key="option.value"
|
||||
:value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<!-- 参数输入区域 -->
|
||||
<div class="left-panel">
|
||||
<div class="variable-inputs">
|
||||
<div v-for="(param, index) in startNodeParams" :key="index" class="input-item">
|
||||
<div class="input-label" :class="{ required: param.required }">
|
||||
{{ param.name }}
|
||||
</div>
|
||||
<div class="input-value">
|
||||
<input
|
||||
v-if="param.inputType === 'input'"
|
||||
v-model="param.value"
|
||||
class="param-input"
|
||||
:disabled="isRunning"
|
||||
:class="{ error: showError && param.required && !param.value }"
|
||||
:placeholder="'请输入' + param.name"
|
||||
/>
|
||||
<input
|
||||
v-else-if="param.inputType === 'number'"
|
||||
type="number"
|
||||
v-model.number="param.value"
|
||||
class="param-input"
|
||||
:disabled="isRunning"
|
||||
:class="{ error: showError && param.required && !param.value }"
|
||||
:placeholder="'请输入' + param.name"
|
||||
/>
|
||||
<textarea
|
||||
v-else-if="param.inputType === 'textarea'"
|
||||
v-model="param.value"
|
||||
class="param-textarea"
|
||||
:disabled="isRunning"
|
||||
:class="{ error: showError && param.required && !param.value }"
|
||||
:placeholder="'请输入' + param.name"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<select
|
||||
v-else-if="param.inputType === 'select'"
|
||||
v-model="param.value"
|
||||
class="param-select"
|
||||
:disabled="isRunning"
|
||||
:class="{ error: showError && param.required && !param.value }"
|
||||
>
|
||||
<option value="">请选择{{ param.name }}</option>
|
||||
<option v-for="option in param.options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button type="primary"
|
||||
class="run-btn"
|
||||
:disabled="isRunning"
|
||||
@click="handleParamRun">
|
||||
{{ isRunning ? '运行中...' : '运行' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" class="run-btn" :disabled="isRunning" @click="handleParamRun">
|
||||
{{ isRunning ? '运行中...' : '运行' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 执行状态和结果区域 -->
|
||||
<div class="execution-detail"
|
||||
v-if="executionNodes.length">
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<div class="label">状态</div>
|
||||
<div class="value"
|
||||
:class="executionStatus.class">
|
||||
{{ executionStatus.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">运行时间</div>
|
||||
<div class="value">{{ formatTotalTime(executionTime) }}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">总 TOKEN 数</div>
|
||||
<div class="value">{{ totalTokens }} Tokens</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<!-- 执行进度和结果区域 -->
|
||||
<node-list :nodes="executionNodes"
|
||||
@end="handleEnd"
|
||||
v-if="executionNodes.length" />
|
||||
<!-- 执行状态和结果区域 -->
|
||||
<div class="execution-detail" v-if="executionNodes.length">
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<div class="label">状态</div>
|
||||
<div class="value" :class="executionStatus.class">
|
||||
{{ executionStatus.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">运行时间</div>
|
||||
<div class="value">{{ formatTotalTime(executionTime) }}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">总 TOKEN 数</div>
|
||||
<div class="value">{{ totalTokens }} Tokens</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<!-- 执行进度和结果区域 -->
|
||||
<node-list :nodes="executionNodes" @end="handleEnd" v-if="executionNodes.length" />
|
||||
|
||||
<!-- 最终执行结果 -->
|
||||
<div class="execution-result"
|
||||
v-if="executionResult">
|
||||
<h4>执行结果</h4>
|
||||
<pre>{{ JSON.stringify(executionResult, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 最终执行结果 -->
|
||||
<div class="execution-result" v-if="executionResult">
|
||||
<h4>执行结果</h4>
|
||||
<pre>{{ JSON.stringify(executionResult, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Loading, Check, CircleClose, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { Loading, Check, CircleClose, ArrowRight } from '@element-plus/icons-vue';
|
||||
import { getObj } from '/@/api/knowledge/aiFlow';
|
||||
import NodeList from './components/NodeList.vue'
|
||||
import NodeCommon from './mixins/Node.ts'
|
||||
import UnpublishedMask from './components/UnpublishedMask.vue'
|
||||
import NodeList from './components/NodeList.vue';
|
||||
import NodeCommon from './mixins/Node.ts';
|
||||
import UnpublishedMask from './components/UnpublishedMask.vue';
|
||||
|
||||
export default {
|
||||
name: 'WorkflowRun',
|
||||
mixins: [NodeCommon],
|
||||
components: {
|
||||
Loading,
|
||||
Check,
|
||||
CircleClose,
|
||||
ArrowRight,
|
||||
NodeList,
|
||||
UnpublishedMask
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
parent: this,
|
||||
nodes: this.nodes,
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
form: { enabled: true },
|
||||
executionNodes: [],
|
||||
executionResult: null,
|
||||
executionTime: 0,
|
||||
totalTokens: 0,
|
||||
startNodeParams: [], // 添加开始节点参数
|
||||
showError: false,
|
||||
isRunning: false // 添加运行状态控制
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
executionStatus () {
|
||||
const lastNode = this.executionNodes[this.executionNodes.length - 1]
|
||||
if (!lastNode) return { text: '等待中', class: 'status-pending' }
|
||||
name: 'WorkflowRun',
|
||||
mixins: [NodeCommon],
|
||||
components: {
|
||||
Loading,
|
||||
Check,
|
||||
CircleClose,
|
||||
ArrowRight,
|
||||
NodeList,
|
||||
UnpublishedMask,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
parent: this,
|
||||
nodes: this.nodes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: { enabled: true },
|
||||
executionNodes: [],
|
||||
executionResult: null,
|
||||
executionTime: 0,
|
||||
totalTokens: 0,
|
||||
startNodeParams: [], // 添加开始节点参数
|
||||
showError: false,
|
||||
isRunning: false, // 添加运行状态控制
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
executionStatus() {
|
||||
const lastNode = this.executionNodes[this.executionNodes.length - 1];
|
||||
if (!lastNode) return { text: '等待中', class: 'status-pending' };
|
||||
|
||||
const statusMap = {
|
||||
'running': { text: '运行中', class: 'status-running' },
|
||||
'success': { text: '成功', class: 'status-success' },
|
||||
'error': { text: '失败', class: 'status-error' },
|
||||
'skipped': { text: '已跳过', class: 'status-skipped' }
|
||||
}
|
||||
const statusMap = {
|
||||
running: { text: '运行中', class: 'status-running' },
|
||||
success: { text: '成功', class: 'status-success' },
|
||||
error: { text: '失败', class: 'status-error' },
|
||||
skipped: { text: '已跳过', class: 'status-skipped' },
|
||||
};
|
||||
|
||||
return statusMap[lastNode.status] || { text: '等待中', class: 'status-pending' }
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.loadFromStorage()
|
||||
},
|
||||
unmounted() {
|
||||
this.resetConversation();
|
||||
},
|
||||
methods: {
|
||||
// 修改 loadFromStorage 方法
|
||||
async loadFromStorage () {
|
||||
try {
|
||||
const res = await getObj(this.id)
|
||||
this.form = res.data.data;
|
||||
const { dsl = '{}' } = this.form
|
||||
const data = JSON.parse(dsl)
|
||||
this.nodes = data.nodes || []
|
||||
this.connections = data.connections || []
|
||||
this.env = data.env || []
|
||||
this.handleRunClick()
|
||||
} catch (error) {
|
||||
console.error('加载工作流失败:', error)
|
||||
}
|
||||
},
|
||||
return statusMap[lastNode.status] || { text: '等待中', class: 'status-pending' };
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadFromStorage();
|
||||
},
|
||||
unmounted() {
|
||||
this.resetConversation();
|
||||
},
|
||||
methods: {
|
||||
// 修改 loadFromStorage 方法
|
||||
async loadFromStorage() {
|
||||
try {
|
||||
const res = await getObj(this.id);
|
||||
this.form = res.data.data;
|
||||
const { dsl = '{}' } = this.form;
|
||||
const data = JSON.parse(dsl);
|
||||
this.nodes = data.nodes || [];
|
||||
this.connections = data.connections || [];
|
||||
this.env = data.env || [];
|
||||
this.handleRunClick();
|
||||
} catch (error) {
|
||||
console.error('加载工作流失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
formatTotalTime (time) {
|
||||
if (!time) return '0ms'
|
||||
return `${Number(time).toFixed(3)}ms`
|
||||
},
|
||||
handleParamRun () {
|
||||
const hasError = this.startNodeParams.some(param => param.required && !param.value)
|
||||
this.showError = hasError
|
||||
formatTotalTime(time) {
|
||||
if (!time) return '0ms';
|
||||
return `${Number(time).toFixed(3)}ms`;
|
||||
},
|
||||
handleParamRun() {
|
||||
const hasError = this.startNodeParams.some((param) => param.required && !param.value);
|
||||
this.showError = hasError;
|
||||
|
||||
if (hasError) {
|
||||
return
|
||||
}
|
||||
if (hasError) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.runWorkflow(this.startNodeParams)
|
||||
this.showError = false
|
||||
},
|
||||
handleEnd (status) {
|
||||
this.isRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
this.runWorkflow(this.startNodeParams);
|
||||
this.showError = false;
|
||||
},
|
||||
handleEnd(status) {
|
||||
this.isRunning = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './styles/flow.scss';
|
||||
.workflow-designer {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #f8f9fc;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #f8f9fc;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.execution-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-left: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
color: #333;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-left: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
color: #333;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
background: #f8f9fc;
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
background: #f8f9fc;
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
padding: 15px;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-right: 1px solid #dcdfe6;
|
||||
padding: 15px;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-right: 1px solid #dcdfe6;
|
||||
}
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.execution-detail {
|
||||
margin-top: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #f0f9eb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e1f3d8;
|
||||
box-sizing: border-box;
|
||||
background: #f0f9eb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e1f3d8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.detail-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid rgba(225, 243, 216, 0.8);
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid rgba(225, 243, 216, 0.8);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.label {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #67c23a;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
.value {
|
||||
color: #67c23a;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
&.status-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.status-error {
|
||||
color: #f56c6c;
|
||||
}
|
||||
&.status-error {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.status-running {
|
||||
color: #409eff;
|
||||
}
|
||||
&.status-running {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
&.status-pending {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.run-btn {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.execution-result {
|
||||
margin-top: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
}
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
pre {
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,138 +1,129 @@
|
||||
<template>
|
||||
<!-- 对话详情抽屉 -->
|
||||
<el-drawer v-model="visible"
|
||||
title="对话详情"
|
||||
size="500"
|
||||
:before-close="handleClose"
|
||||
class="chat-detail-drawer">
|
||||
<div v-loading="loading">
|
||||
<ChatMessage :messages="messages"
|
||||
v-if="messages.length > 0"
|
||||
ref="messageContainer" />
|
||||
<el-empty v-else
|
||||
description="暂无对话记录">
|
||||
</el-empty>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<!-- 对话详情抽屉 -->
|
||||
<el-drawer v-model="visible" title="对话详情" size="500" :before-close="handleClose" class="chat-detail-drawer">
|
||||
<div v-loading="loading">
|
||||
<ChatMessage :messages="messages" v-if="messages.length > 0" ref="messageContainer" />
|
||||
<el-empty v-else description="暂无对话记录"> </el-empty>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { marked } from 'marked'
|
||||
import { Cpu } from '@element-plus/icons-vue'
|
||||
import ChatMessage from './ChatMessage.vue'
|
||||
import { marked } from 'marked';
|
||||
import { Cpu } from '@element-plus/icons-vue';
|
||||
import ChatMessage from './ChatMessage.vue';
|
||||
export default {
|
||||
name: 'ChatDetail',
|
||||
components: {
|
||||
Cpu,
|
||||
ChatMessage
|
||||
},
|
||||
name: 'ChatDetail',
|
||||
components: {
|
||||
Cpu,
|
||||
ChatMessage,
|
||||
},
|
||||
|
||||
props: {
|
||||
// 对话ID
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 是否显示弹窗
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
// 对话ID
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
// 是否显示弹窗
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
messages: []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
messages: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
visible: {
|
||||
get () {
|
||||
return this.modelValue
|
||||
},
|
||||
set (val) {
|
||||
this.$emit('update:modelValue', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:modelValue', val);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
conversationId: {
|
||||
handler (val) {
|
||||
if (val) {
|
||||
this.loadMessages()
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
conversationId: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.loadMessages();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载消息列表
|
||||
async loadMessages () {
|
||||
if (!this.conversationId) return
|
||||
methods: {
|
||||
// 加载消息列表
|
||||
async loadMessages() {
|
||||
if (!this.conversationId) return;
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const res = null
|
||||
this.messages = res.data.data.records || []
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error)
|
||||
this.$message.error('加载消息失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = null;
|
||||
this.messages = res.data.data.records || [];
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error);
|
||||
this.$message.error('加载消息失败');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 解析 Markdown
|
||||
parseMarkdown (text) {
|
||||
if (!text) return ''
|
||||
return marked(text)
|
||||
},
|
||||
// 解析 Markdown
|
||||
parseMarkdown(text) {
|
||||
if (!text) return '';
|
||||
return marked(text);
|
||||
},
|
||||
|
||||
// 关闭弹窗
|
||||
handleClose () {
|
||||
this.visible = false
|
||||
},
|
||||
// 关闭弹窗
|
||||
handleClose() {
|
||||
this.visible = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理消息编辑事件
|
||||
* @param {Object} data - 包含索引和更新后消息的对象
|
||||
*/
|
||||
handleMessageEdited(data) {
|
||||
// 更新消息数组中的消息
|
||||
if (data && data.index >= 0 && data.index < this.messages.length) {
|
||||
this.messages[data.index] = data.message;
|
||||
/**
|
||||
* 处理消息编辑事件
|
||||
* @param {Object} data - 包含索引和更新后消息的对象
|
||||
*/
|
||||
handleMessageEdited(data) {
|
||||
// 更新消息数组中的消息
|
||||
if (data && data.index >= 0 && data.index < this.messages.length) {
|
||||
this.messages[data.index] = data.message;
|
||||
|
||||
// 发送更新事件给父组件
|
||||
this.$emit('message-updated', this.messages);
|
||||
}
|
||||
},
|
||||
// 发送更新事件给父组件
|
||||
this.$emit('message-updated', this.messages);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理消息删除事件
|
||||
* @param {Number} index - 要删除的消息索引
|
||||
*/
|
||||
handleMessageDeleted(index) {
|
||||
// 从消息数组中删除消息
|
||||
if (index >= 0 && index < this.messages.length) {
|
||||
this.messages.splice(index, 1);
|
||||
/**
|
||||
* 处理消息删除事件
|
||||
* @param {Number} index - 要删除的消息索引
|
||||
*/
|
||||
handleMessageDeleted(index) {
|
||||
// 从消息数组中删除消息
|
||||
if (index >= 0 && index < this.messages.length) {
|
||||
this.messages.splice(index, 1);
|
||||
|
||||
// 发送更新事件给父组件
|
||||
this.$emit('message-updated', this.messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 发送更新事件给父组件
|
||||
this.$emit('message-updated', this.messages);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.chat-detail-drawer {
|
||||
.el-drawer__body{
|
||||
padding: 0;
|
||||
}
|
||||
.el-drawer__body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,222 +1,189 @@
|
||||
<template>
|
||||
<div class="messages">
|
||||
<div v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
class="message"
|
||||
:class="[msg.role]"
|
||||
@contextmenu.prevent="showContextMenu($event, msg, index)">
|
||||
<div class="avatar">
|
||||
<img :src="msg.role === 'user' ? currentUserAvatar : botAvatar"
|
||||
alt="avatar" />
|
||||
</div>
|
||||
<div style="width: 100%;">
|
||||
<!-- 消息时间显示 - 移到content外面 -->
|
||||
<div class="time">{{ getMessageTime(msg) }}</div>
|
||||
<div class="content">
|
||||
<!-- 思考内容区域,添加可折叠功能,默认展开 -->
|
||||
<div class="collapsible_wrapper"
|
||||
v-if="msg.reasoning_content">
|
||||
<div class="collapsible_tag"
|
||||
@click="toggleContent(index, 'reasoning')">
|
||||
<el-icon class="collapsible-icon"
|
||||
:class="{ 'is-rotate': contentVisible[index]?.reasoning !== false }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
<span>深度思考</span>
|
||||
</div>
|
||||
<div class="collapsible_content"
|
||||
v-show="contentVisible[index]?.reasoning !== false">
|
||||
{{ msg.reasoning_content }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="msg.content">
|
||||
<div v-html="parseMarkdown(msg.content)"></div>
|
||||
<div class="questions"
|
||||
v-if="msg.questions">
|
||||
<el-tag v-for="(question, qIndex) in msg.questions"
|
||||
:key="qIndex"
|
||||
type="primary"
|
||||
@click="handleQuestionClick(question.text)">
|
||||
{{ question.text }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="msg.nodes"
|
||||
class="collapsible_wrapper">
|
||||
<div class="collapsible_tag"
|
||||
@click="toggleContent(index, 'nodes')">
|
||||
<el-icon class="collapsible-icon"
|
||||
:class="{ 'is-rotate': contentVisible[index]?.nodes !== false }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
<span>执行步骤</span>
|
||||
</div>
|
||||
<div class="collapsible_content"
|
||||
v-show="contentVisible[index]?.nodes !== false">
|
||||
<node-list :nodes="msg.nodes" />
|
||||
</div>
|
||||
<div v-if="msg.result"
|
||||
v-html="parseMarkdown(msg.result)">
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="loading && messages.indexOf(msg) === messages.length - 1">
|
||||
<div class="typing-indicator">
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右键菜单 -->
|
||||
<el-dialog v-model="editDialogVisible"
|
||||
title="编辑消息"
|
||||
width="50%"
|
||||
:before-close="handleEditDialogClose">
|
||||
<el-input v-model="editingContent"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请输入消息内容" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleEditDialogClose">取消</el-button>
|
||||
<el-button type="primary"
|
||||
@click="saveEditedMessage">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 自定义右键菜单 -->
|
||||
<div v-show="contextMenuVisible"
|
||||
class="context-menu"
|
||||
:style="{ top: contextMenuTop + 'px', left: contextMenuLeft + 'px' }">
|
||||
<div class="context-menu-item"
|
||||
@click="copyMessage">
|
||||
<el-icon>
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span>复制</span>
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
@click="editMessage">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<span>编辑</span>
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
@click="deleteMessage">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
<span>删除</span>
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
@click="speakMessage">
|
||||
<el-icon>
|
||||
<Microphone />
|
||||
</el-icon>
|
||||
<span>朗读</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messages">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="message" :class="[msg.role]" @contextmenu.prevent="showContextMenu($event, msg, index)">
|
||||
<div class="avatar">
|
||||
<img :src="msg.role === 'user' ? currentUserAvatar : botAvatar" alt="avatar" />
|
||||
</div>
|
||||
<div style="width: 100%">
|
||||
<!-- 消息时间显示 - 移到content外面 -->
|
||||
<div class="time">{{ getMessageTime(msg) }}</div>
|
||||
<div class="content">
|
||||
<!-- 思考内容区域,添加可折叠功能,默认展开 -->
|
||||
<div class="collapsible_wrapper" v-if="msg.reasoning_content">
|
||||
<div class="collapsible_tag" @click="toggleContent(index, 'reasoning')">
|
||||
<el-icon class="collapsible-icon" :class="{ 'is-rotate': contentVisible[index]?.reasoning !== false }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
<span>深度思考</span>
|
||||
</div>
|
||||
<div class="collapsible_content" v-show="contentVisible[index]?.reasoning !== false">
|
||||
{{ msg.reasoning_content }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="msg.content">
|
||||
<div v-html="parseMarkdown(msg.content)"></div>
|
||||
<div class="questions" v-if="msg.questions">
|
||||
<el-tag v-for="(question, qIndex) in msg.questions" :key="qIndex" type="primary" @click="handleQuestionClick(question.text)">
|
||||
{{ question.text }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="msg.nodes" class="collapsible_wrapper">
|
||||
<div class="collapsible_tag" @click="toggleContent(index, 'nodes')">
|
||||
<el-icon class="collapsible-icon" :class="{ 'is-rotate': contentVisible[index]?.nodes !== false }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
<span>执行步骤</span>
|
||||
</div>
|
||||
<div class="collapsible_content" v-show="contentVisible[index]?.nodes !== false">
|
||||
<node-list :nodes="msg.nodes" />
|
||||
</div>
|
||||
<div v-if="msg.result" v-html="parseMarkdown(msg.result)"></div>
|
||||
</div>
|
||||
<template v-if="loading && messages.indexOf(msg) === messages.length - 1">
|
||||
<div class="typing-indicator">
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右键菜单 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑消息" width="50%" :before-close="handleEditDialogClose">
|
||||
<el-input v-model="editingContent" type="textarea" :rows="10" placeholder="请输入消息内容" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleEditDialogClose">取消</el-button>
|
||||
<el-button type="primary" @click="saveEditedMessage">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 自定义右键菜单 -->
|
||||
<div v-show="contextMenuVisible" class="context-menu" :style="{ top: contextMenuTop + 'px', left: contextMenuLeft + 'px' }">
|
||||
<div class="context-menu-item" @click="copyMessage">
|
||||
<el-icon>
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span>复制</span>
|
||||
</div>
|
||||
<div class="context-menu-item" @click="editMessage">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<span>编辑</span>
|
||||
</div>
|
||||
<div class="context-menu-item" @click="deleteMessage">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
<span>删除</span>
|
||||
</div>
|
||||
<div class="context-menu-item" @click="speakMessage">
|
||||
<el-icon>
|
||||
<Microphone />
|
||||
</el-icon>
|
||||
<span>朗读</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js/lib/core'
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
// 按需导入常用的语言
|
||||
import javascript from 'highlight.js/lib/languages/javascript'
|
||||
import typescript from 'highlight.js/lib/languages/typescript'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
import java from 'highlight.js/lib/languages/java'
|
||||
import xml from 'highlight.js/lib/languages/xml'
|
||||
import css from 'highlight.js/lib/languages/css'
|
||||
import scss from 'highlight.js/lib/languages/scss'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
import bash from 'highlight.js/lib/languages/bash'
|
||||
import markdown from 'highlight.js/lib/languages/markdown'
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
import python from 'highlight.js/lib/languages/python';
|
||||
import java from 'highlight.js/lib/languages/java';
|
||||
import xml from 'highlight.js/lib/languages/xml';
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
import scss from 'highlight.js/lib/languages/scss';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import bash from 'highlight.js/lib/languages/bash';
|
||||
import markdown from 'highlight.js/lib/languages/markdown';
|
||||
// 导入暗色主题样式
|
||||
import 'highlight.js/styles/atom-one-dark.css'
|
||||
import 'highlight.js/styles/atom-one-dark.css';
|
||||
// 导入Element Plus图标
|
||||
import { ArrowDown, Document, Edit, Delete, Microphone } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowDown, Document, Edit, Delete, Microphone } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import NodeList from '/@/views/knowledge/aiFlow/components/NodeList.vue'
|
||||
import {dateTimeStr} from "/@/utils/formatTime";
|
||||
import NodeList from '/@/views/knowledge/aiFlow/components/NodeList.vue';
|
||||
import { dateTimeStr } from '/@/utils/formatTime';
|
||||
|
||||
// 定义组件接收的props
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
messages: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
currentUserAvatar: {
|
||||
type: String,
|
||||
default: '/img/chat/icon.png'
|
||||
},
|
||||
botAvatar: {
|
||||
type: String,
|
||||
default: '/img/chat/chatgpt.png'
|
||||
}
|
||||
})
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
messages: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currentUserAvatar: {
|
||||
type: String,
|
||||
default: '/img/chat/icon.png',
|
||||
},
|
||||
botAvatar: {
|
||||
type: String,
|
||||
default: '/img/chat/chatgpt.png',
|
||||
},
|
||||
});
|
||||
|
||||
// 定义组件触发的事件
|
||||
const emit = defineEmits(['item-click', 'change'])
|
||||
const emit = defineEmits(['item-click', 'change']);
|
||||
|
||||
// 注册语言
|
||||
hljs.registerLanguage('javascript', javascript)
|
||||
hljs.registerLanguage('typescript', typescript)
|
||||
hljs.registerLanguage('python', python)
|
||||
hljs.registerLanguage('java', java)
|
||||
hljs.registerLanguage('xml', xml)
|
||||
hljs.registerLanguage('css', css)
|
||||
hljs.registerLanguage('scss', scss)
|
||||
hljs.registerLanguage('json', json)
|
||||
hljs.registerLanguage('bash', bash)
|
||||
hljs.registerLanguage('markdown', markdown)
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
hljs.registerLanguage('python', python);
|
||||
hljs.registerLanguage('java', java);
|
||||
hljs.registerLanguage('xml', xml);
|
||||
hljs.registerLanguage('css', css);
|
||||
hljs.registerLanguage('scss', scss);
|
||||
hljs.registerLanguage('json', json);
|
||||
hljs.registerLanguage('bash', bash);
|
||||
hljs.registerLanguage('markdown', markdown);
|
||||
|
||||
// 配置marked的代码高亮选项
|
||||
onMounted(() => {
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return code
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value
|
||||
},
|
||||
breaks: true, // 支持换行符
|
||||
gfm: true, // 启用GitHub风格Markdown
|
||||
sanitize: false, // 允许HTML标签以支持代码高亮
|
||||
langPrefix: 'hljs language-', // 添加代码块的class前缀
|
||||
})
|
||||
})
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
breaks: true, // 支持换行符
|
||||
gfm: true, // 启用GitHub风格Markdown
|
||||
sanitize: false, // 允许HTML标签以支持代码高亮
|
||||
langPrefix: 'hljs language-', // 添加代码块的class前缀
|
||||
});
|
||||
});
|
||||
|
||||
// 使用reactive创建响应式对象,用于跟踪内容的显示状态
|
||||
const contentVisible = reactive({})
|
||||
const contentVisible = reactive({});
|
||||
|
||||
// 右键菜单相关状态
|
||||
const contextMenuVisible = ref(false)
|
||||
const contextMenuTop = ref(0)
|
||||
const contextMenuLeft = ref(0)
|
||||
const currentMessage = ref(null)
|
||||
const currentMessageIndex = ref(-1)
|
||||
const contextMenuVisible = ref(false);
|
||||
const contextMenuTop = ref(0);
|
||||
const contextMenuLeft = ref(0);
|
||||
const currentMessage = ref(null);
|
||||
const currentMessageIndex = ref(-1);
|
||||
|
||||
// 编辑对话框相关状态
|
||||
const editDialogVisible = ref(false)
|
||||
const editingContent = ref('')
|
||||
const editDialogVisible = ref(false);
|
||||
const editingContent = ref('');
|
||||
|
||||
/**
|
||||
* 切换内容的显示/隐藏状态
|
||||
@@ -224,11 +191,11 @@ const editingContent = ref('')
|
||||
* @param {string} type - 内容类型 ('reasoning' 或 'nodes')
|
||||
*/
|
||||
const toggleContent = (index, type) => {
|
||||
if (!contentVisible[index]) {
|
||||
contentVisible[index] = {}
|
||||
}
|
||||
contentVisible[index][type] = contentVisible[index][type] === false ? true : false
|
||||
}
|
||||
if (!contentVisible[index]) {
|
||||
contentVisible[index] = {};
|
||||
}
|
||||
contentVisible[index][type] = contentVisible[index][type] === false ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取消息的时间
|
||||
@@ -236,17 +203,16 @@ const toggleContent = (index, type) => {
|
||||
* @returns {string} - 格式化后的时间字符串
|
||||
*/
|
||||
const getMessageTime = (msg) => {
|
||||
// 如果消息对象中已有时间属性,则直接使用
|
||||
if (msg.time) {
|
||||
|
||||
return parseDate(msg.time,dateTimeStr)
|
||||
}
|
||||
// 否则生成当前时间并赋值给消息对象
|
||||
const currentTime = parseDate(new Date(),dateTimeStr)
|
||||
// 为消息对象添加时间属性
|
||||
msg.time = new Date().toISOString()
|
||||
return currentTime
|
||||
}
|
||||
// 如果消息对象中已有时间属性,则直接使用
|
||||
if (msg.time) {
|
||||
return parseDate(msg.time, dateTimeStr);
|
||||
}
|
||||
// 否则生成当前时间并赋值给消息对象
|
||||
const currentTime = parseDate(new Date(), dateTimeStr);
|
||||
// 为消息对象添加时间属性
|
||||
msg.time = new Date().toISOString();
|
||||
return currentTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示右键菜单
|
||||
@@ -255,153 +221,148 @@ const getMessageTime = (msg) => {
|
||||
* @param {number} index - 消息索引
|
||||
*/
|
||||
const showContextMenu = (event, msg, index) => {
|
||||
// 阻止默认右键菜单
|
||||
event.preventDefault()
|
||||
// 设置菜单位置
|
||||
contextMenuTop.value = event.clientY
|
||||
contextMenuLeft.value = event.clientX
|
||||
// 保存当前消息和索引
|
||||
currentMessage.value = msg
|
||||
currentMessageIndex.value = index
|
||||
// 显示菜单
|
||||
contextMenuVisible.value = true
|
||||
}
|
||||
// 阻止默认右键菜单
|
||||
event.preventDefault();
|
||||
// 设置菜单位置
|
||||
contextMenuTop.value = event.clientY;
|
||||
contextMenuLeft.value = event.clientX;
|
||||
// 保存当前消息和索引
|
||||
currentMessage.value = msg;
|
||||
currentMessageIndex.value = index;
|
||||
// 显示菜单
|
||||
contextMenuVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 隐藏右键菜单
|
||||
*/
|
||||
const hideContextMenu = () => {
|
||||
contextMenuVisible.value = false
|
||||
}
|
||||
contextMenuVisible.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 复制消息内容到剪贴板
|
||||
*/
|
||||
const copyMessage = () => {
|
||||
if (!currentMessage.value) return
|
||||
if (!currentMessage.value) return;
|
||||
|
||||
// 获取要复制的文本内容
|
||||
const textToCopy = currentMessage.value.content ||
|
||||
currentMessage.value.reasoning_content ||
|
||||
(currentMessage.value.result || '')
|
||||
// 获取要复制的文本内容
|
||||
const textToCopy = currentMessage.value.content || currentMessage.value.reasoning_content || currentMessage.value.result || '';
|
||||
|
||||
// 使用Clipboard API复制文本
|
||||
navigator.clipboard.writeText(textToCopy)
|
||||
.then(() => {
|
||||
ElMessage.success('复制成功')
|
||||
})
|
||||
.catch(err => {
|
||||
ElMessage.error('复制失败: ' + err)
|
||||
})
|
||||
// 使用Clipboard API复制文本
|
||||
navigator.clipboard
|
||||
.writeText(textToCopy)
|
||||
.then(() => {
|
||||
ElMessage.success('复制成功');
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error('复制失败: ' + err);
|
||||
});
|
||||
|
||||
// 隐藏菜单
|
||||
hideContextMenu()
|
||||
}
|
||||
// 隐藏菜单
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开编辑对话框
|
||||
*/
|
||||
const editMessage = () => {
|
||||
if (!currentMessage.value) return
|
||||
if (!currentMessage.value) return;
|
||||
|
||||
// 设置编辑内容
|
||||
editingContent.value = currentMessage.value.content ||
|
||||
currentMessage.value.reasoning_content ||
|
||||
(currentMessage.value.result || '')
|
||||
// 设置编辑内容
|
||||
editingContent.value = currentMessage.value.content || currentMessage.value.reasoning_content || currentMessage.value.result || '';
|
||||
|
||||
// 显示编辑对话框
|
||||
editDialogVisible.value = true
|
||||
// 显示编辑对话框
|
||||
editDialogVisible.value = true;
|
||||
|
||||
// 隐藏菜单
|
||||
hideContextMenu()
|
||||
}
|
||||
// 隐藏菜单
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭编辑对话框
|
||||
*/
|
||||
const handleEditDialogClose = () => {
|
||||
editDialogVisible.value = false
|
||||
editingContent.value = ''
|
||||
}
|
||||
editDialogVisible.value = false;
|
||||
editingContent.value = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存编辑后的消息
|
||||
*/
|
||||
const saveEditedMessage = () => {
|
||||
if (currentMessageIndex.value === -1 || !currentMessage.value) return
|
||||
if (currentMessageIndex.value === -1 || !currentMessage.value) return;
|
||||
|
||||
// 更新时间
|
||||
currentMessage.value.time = dayjs().toISOString()
|
||||
// 更新消息内容
|
||||
currentMessage.value.content = editingContent.value
|
||||
// 更新时间
|
||||
currentMessage.value.time = dayjs().toISOString();
|
||||
// 更新消息内容
|
||||
currentMessage.value.content = editingContent.value;
|
||||
|
||||
// 发送编辑消息事件
|
||||
emit('change')
|
||||
// 发送编辑消息事件
|
||||
emit('change');
|
||||
|
||||
// 关闭对话框
|
||||
handleEditDialogClose()
|
||||
// 关闭对话框
|
||||
handleEditDialogClose();
|
||||
|
||||
ElMessage.success('消息已更新')
|
||||
}
|
||||
ElMessage.success('消息已更新');
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
*/
|
||||
const deleteMessage = () => {
|
||||
if (currentMessageIndex.value === -1) return
|
||||
if (currentMessageIndex.value === -1) return;
|
||||
|
||||
// 从消息数组中删除当前消息
|
||||
props.messages.splice(currentMessageIndex.value, 1)
|
||||
// 从消息数组中删除当前消息
|
||||
props.messages.splice(currentMessageIndex.value, 1);
|
||||
|
||||
// 发送删除消息事件
|
||||
emit('change')
|
||||
// 发送删除消息事件
|
||||
emit('change');
|
||||
|
||||
// 隐藏菜单
|
||||
hideContextMenu()
|
||||
// 隐藏菜单
|
||||
hideContextMenu();
|
||||
|
||||
ElMessage.success('消息已删除')
|
||||
}
|
||||
ElMessage.success('消息已删除');
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用浏览器API朗读消息
|
||||
*/
|
||||
const speakMessage = () => {
|
||||
if (!currentMessage.value) return
|
||||
if (!currentMessage.value) return;
|
||||
|
||||
// 获取要朗读的文本
|
||||
const textToSpeak = currentMessage.value.content ||
|
||||
currentMessage.value.reasoning_content ||
|
||||
(currentMessage.value.result || '')
|
||||
// 获取要朗读的文本
|
||||
const textToSpeak = currentMessage.value.content || currentMessage.value.reasoning_content || currentMessage.value.result || '';
|
||||
|
||||
// 检查浏览器是否支持语音合成
|
||||
if ('speechSynthesis' in window) {
|
||||
// 创建语音合成实例
|
||||
const utterance = new SpeechSynthesisUtterance(textToSpeak)
|
||||
// 检查浏览器是否支持语音合成
|
||||
if ('speechSynthesis' in window) {
|
||||
// 创建语音合成实例
|
||||
const utterance = new SpeechSynthesisUtterance(textToSpeak);
|
||||
|
||||
// 设置语音属性
|
||||
utterance.lang = 'zh-CN' // 设置语言为中文
|
||||
utterance.rate = 1.0 // 设置语速
|
||||
utterance.pitch = 1.0 // 设置音调
|
||||
// 设置语音属性
|
||||
utterance.lang = 'zh-CN'; // 设置语言为中文
|
||||
utterance.rate = 1.0; // 设置语速
|
||||
utterance.pitch = 1.0; // 设置音调
|
||||
|
||||
// 开始朗读
|
||||
window.speechSynthesis.speak(utterance)
|
||||
// 开始朗读
|
||||
window.speechSynthesis.speak(utterance);
|
||||
|
||||
ElMessage.success('正在朗读消息')
|
||||
} else {
|
||||
ElMessage.error('您的浏览器不支持语音合成')
|
||||
}
|
||||
ElMessage.success('正在朗读消息');
|
||||
} else {
|
||||
ElMessage.error('您的浏览器不支持语音合成');
|
||||
}
|
||||
|
||||
// 隐藏菜单
|
||||
hideContextMenu()
|
||||
}
|
||||
// 隐藏菜单
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理问题点击事件
|
||||
* @param {string} text - 问题文本
|
||||
*/
|
||||
const handleQuestionClick = (text) => {
|
||||
emit('item-click', text)
|
||||
}
|
||||
emit('item-click', text);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析Markdown内容并支持代码高亮
|
||||
@@ -409,162 +370,161 @@ const handleQuestionClick = (text) => {
|
||||
* @returns {string} - 解析后的HTML字符串
|
||||
*/
|
||||
const parseMarkdown = (content) => {
|
||||
if (!content) return ''
|
||||
return marked(content)
|
||||
}
|
||||
if (!content) return '';
|
||||
return marked(content);
|
||||
};
|
||||
|
||||
// 点击页面其他区域时隐藏右键菜单
|
||||
const handleDocumentClick = () => {
|
||||
hideContextMenu()
|
||||
}
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
// 组件挂载时添加全局点击事件监听
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleDocumentClick)
|
||||
})
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
});
|
||||
|
||||
// 组件卸载前移除全局点击事件监听
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
})
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 可折叠内容区域通用样式
|
||||
.collapsible_wrapper {
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsible_tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0px 8px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0px 8px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #e8e8e8;
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #e8e8e8;
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
|
||||
.collapsible-icon {
|
||||
margin-right: 6px;
|
||||
transition: transform 0.3s;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
.collapsible-icon {
|
||||
margin-right: 6px;
|
||||
transition: transform 0.3s;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
|
||||
&.is-rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
&.is-rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible_content {
|
||||
position: relative;
|
||||
margin: 5px 0;
|
||||
padding: 0px 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: ' ';
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
position: relative;
|
||||
margin: 5px 0;
|
||||
padding: 0px 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: ' ';
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单样式
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 0;
|
||||
min-width: 120px;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color 0.3s;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background-color: #282c34; // Atom One Dark 背景色
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
background-color: #282c34; // Atom One Dark 背景色
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: #3a404b; // Atom One Dark 次要背景色
|
||||
color: #abb2bf; // Atom One Dark 文本颜色
|
||||
border-radius: 3px;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: #3a404b; // Atom One Dark 次要背景色
|
||||
color: #abb2bf; // Atom One Dark 文本颜色
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:deep(pre code) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #abb2bf; // Atom One Dark 文本颜色
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #abb2bf; // Atom One Dark 文本颜色
|
||||
}
|
||||
|
||||
// 添加代码块的滚动条样式
|
||||
:deep(pre)::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
:deep(pre)::-webkit-scrollbar-thumb {
|
||||
background: #3a404b;
|
||||
border-radius: 4px;
|
||||
background: #3a404b;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(pre)::-webkit-scrollbar-track {
|
||||
background: #282c34;
|
||||
border-radius: 4px;
|
||||
background: #282c34;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
:start-params="startNodeParams"
|
||||
:conversation-id="conversationId"
|
||||
@run="runWorkflow"
|
||||
@update:stream="(value) => isStream = value"
|
||||
@update:stream="(value) => (isStream = value)"
|
||||
@close="showExecutionPanel = false"
|
||||
/>
|
||||
</template>
|
||||
@@ -302,7 +302,7 @@ export default {
|
||||
if (sourceNode) {
|
||||
// 获取源节点已有的连接数量
|
||||
const existingConnections = this.connections.filter((conn) => conn.sourceId === params.sourceId);
|
||||
|
||||
|
||||
// 如果不是分支节点(switch 或 question),且已经有连接,则不允许创建新连接
|
||||
if (!['switch', 'question'].includes(sourceNode.type) && existingConnections.length > 0) {
|
||||
this.$message.warning('除分支节点外,每个节点只能有一个子节点');
|
||||
@@ -462,7 +462,7 @@ export default {
|
||||
if (this.contextMenuAddPosition === 'right') {
|
||||
// 获取当前节点已有的连接数量
|
||||
const existingConnections = this.connections.filter((conn) => conn.sourceId === this.contextMenuNode.id);
|
||||
|
||||
|
||||
// 如果不是分支节点且已经有连接,则不允许添加
|
||||
if (!['switch', 'question'].includes(this.contextMenuNode.type) && existingConnections.length > 0) {
|
||||
this.$message.warning('除分支节点外,每个节点只能有一个子节点');
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
|
||||
<div class="w-px h-4 mx-2 bg-gray-200 dark:bg-gray-700"></div>
|
||||
|
||||
|
||||
<el-button
|
||||
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
text
|
||||
|
||||
@@ -246,7 +246,7 @@ export default {
|
||||
},
|
||||
onProgress: (event: FlowExecutionEvent) => {
|
||||
this.$log?.info?.(`节点执行进度: ${event.nodeName} (${event.nodeId})`);
|
||||
|
||||
|
||||
// 更新执行节点状态
|
||||
if (event.nodeId && this.executionNodes) {
|
||||
const nodeIndex = this.executionNodes.findIndex((n: ExecutionNode) => n.id === event.nodeId);
|
||||
@@ -255,12 +255,12 @@ export default {
|
||||
this.executionNodes[nodeIndex] = {
|
||||
...this.executionNodes[nodeIndex],
|
||||
status: 'running',
|
||||
...event.data
|
||||
...event.data,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 可以在这里添加进度条或其他UI更新
|
||||
if (event.progress !== undefined) {
|
||||
this.$log?.info?.(`执行进度: ${event.progress}%`);
|
||||
@@ -270,7 +270,7 @@ export default {
|
||||
// 累积聊天消息内容
|
||||
chatMessageContent += content;
|
||||
isChatStreaming = true;
|
||||
|
||||
|
||||
// 处理 tokens、duration 和 nodes 信息
|
||||
if (isComplete) {
|
||||
if (tokens) {
|
||||
@@ -283,19 +283,19 @@ export default {
|
||||
this.executionNodes = nodes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 实时更新executionResult以显示聊天消息
|
||||
this.$nextTick(() => {
|
||||
this.executionResult = {
|
||||
...this.executionResult,
|
||||
chatMessage: chatMessageContent,
|
||||
isStreaming: !isComplete
|
||||
isStreaming: !isComplete,
|
||||
};
|
||||
});
|
||||
},
|
||||
onComplete: (result: FlowExecutionResult) => {
|
||||
this.$log?.info?.('工作流执行完成');
|
||||
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.executionNodes = result.nodes;
|
||||
// 如果有聊天消息,将其合并到结果中
|
||||
@@ -303,7 +303,7 @@ export default {
|
||||
this.executionResult = {
|
||||
...result.result,
|
||||
chatMessage: chatMessageContent,
|
||||
isStreaming: false
|
||||
isStreaming: false,
|
||||
};
|
||||
} else {
|
||||
this.executionResult = result.result;
|
||||
@@ -315,7 +315,7 @@ export default {
|
||||
},
|
||||
onError: (error: string) => {
|
||||
this.$log?.error?.('工作流执行失败:', error);
|
||||
|
||||
|
||||
this.$nextTick(() => {
|
||||
// 如果有部分聊天消息,也保留在错误结果中
|
||||
const errorResult: any = { error };
|
||||
@@ -339,7 +339,7 @@ export default {
|
||||
this.executionResult = {
|
||||
chatMessage: chatResult.chatMessage,
|
||||
result: chatResult.result,
|
||||
isStreaming: false
|
||||
isStreaming: false,
|
||||
};
|
||||
this.isRunning = false;
|
||||
});
|
||||
@@ -350,11 +350,11 @@ export default {
|
||||
* @param {ExecutionContext} context - 执行上下文
|
||||
*/
|
||||
async executeWithHTTP(this: any, context: ExecutionContext): Promise<void> {
|
||||
const { data } = await executeFlow({
|
||||
id: this.id,
|
||||
params: context.params,
|
||||
envs: context.envs,
|
||||
stream: false
|
||||
const { data } = await executeFlow({
|
||||
id: this.id,
|
||||
params: context.params,
|
||||
envs: context.envs,
|
||||
stream: false,
|
||||
});
|
||||
|
||||
// 处理普通JSON响应
|
||||
@@ -389,7 +389,7 @@ export default {
|
||||
this.executionResult = {
|
||||
...this.executionResult,
|
||||
chatMessage: chatMessage,
|
||||
isStreaming: false
|
||||
isStreaming: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,54 @@
|
||||
<template>
|
||||
<div class="output-params"
|
||||
v-if="node.method || node.httpParams.url">
|
||||
<div class="param-item">
|
||||
<span class="param-name">{{ node.httpParams.method }}</span>
|
||||
<span class="param-value">{{ node.httpParams.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="output-params" v-if="node.method || node.httpParams.url">
|
||||
<div class="param-item">
|
||||
<span class="param-name">{{ node.httpParams.method }}</span>
|
||||
<span class="param-value">{{ node.httpParams.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import common from './common.ts'
|
||||
import common from './common.ts';
|
||||
export default {
|
||||
name: 'HttpNode',
|
||||
mixins: [common]
|
||||
}
|
||||
name: 'HttpNode',
|
||||
mixins: [common],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 添加样式 */
|
||||
.output-params {
|
||||
padding: 8px 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.param-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
padding: 3px 10px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background-color: rgb(242, 244, 247);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
padding: 3px 10px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background-color: rgb(242, 244, 247);
|
||||
overflow: hidden;
|
||||
}
|
||||
.param-icon {
|
||||
color: rgb(41 112 255);
|
||||
font-weight: bold;
|
||||
color: rgb(41 112 255);
|
||||
font-weight: bold;
|
||||
}
|
||||
.param-name {
|
||||
padding: 2px 5px;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
margin-right: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
padding: 2px 5px;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
margin-right: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -57,4 +57,4 @@ export default {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -85,4 +85,4 @@ export default {
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,17 +2,17 @@ import { Node } from '../types/node';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
export default {
|
||||
inject: ['parent'],
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<Node>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputParams: this.node.inputParams || [],
|
||||
outputParams: this.node.outputParams || []
|
||||
}
|
||||
}
|
||||
}
|
||||
inject: ['parent'],
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<Node>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputParams: this.node.inputParams || [],
|
||||
outputParams: this.node.outputParams || [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ export const nodeTypes: NodeType[] = [
|
||||
{
|
||||
name: '分支2',
|
||||
value: 1,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
outputParams: [
|
||||
@@ -151,7 +151,7 @@ export const nodeTypes: NodeType[] = [
|
||||
{
|
||||
name: 'result',
|
||||
type: 'String',
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -245,7 +245,8 @@ export const nodeTypes: NodeType[] = [
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: '你是一个问题总结助手。\n任务:根据用户提问 ${arg1} 和系统答案 ${arg2},生成一个标准的问题答案。\n要求:基于系统答案内容回答,回答准确、简洁、有用。',
|
||||
content:
|
||||
'你是一个问题总结助手。\n任务:根据用户提问 ${arg1} 和系统答案 ${arg2},生成一个标准的问题答案。\n要求:基于系统答案内容回答,回答准确、简洁、有用。',
|
||||
},
|
||||
],
|
||||
modelConfig: {
|
||||
|
||||
@@ -85,11 +85,10 @@
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import common from './common.ts';
|
||||
import './panel.css';
|
||||
import {list} from "/@/api/gen/datasource";
|
||||
import { list } from '/@/api/gen/datasource';
|
||||
import CodeEditor from '/@/views/knowledge/aiFlow/components/CodeEditor.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'DbPanel',
|
||||
components: {
|
||||
@@ -100,8 +99,7 @@ export default {
|
||||
mixins: [common],
|
||||
data() {
|
||||
return {
|
||||
dbList: [
|
||||
],
|
||||
dbList: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -109,8 +107,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async loadDbList() {
|
||||
const {data} = await list()
|
||||
this.dbList = data
|
||||
const { data } = await list();
|
||||
this.dbList = data;
|
||||
},
|
||||
handleDbChange() {
|
||||
if (!this.node.dbParams.dbId) {
|
||||
@@ -126,7 +124,7 @@ export default {
|
||||
setup() {
|
||||
const result = ref(null);
|
||||
return {
|
||||
result
|
||||
result,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,4 +249,4 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -60,12 +60,12 @@
|
||||
<div class="param-item param-item-margin" v-if="node.mcpParams.mcpId">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">请求提示词:</span>
|
||||
<el-input
|
||||
v-model="node.mcpParams.prompt"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请求提示词,使用${变量名}引用上方定义的变量"
|
||||
class="flex-1"
|
||||
<el-input
|
||||
v-model="node.mcpParams.prompt"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请求提示词,使用${变量名}引用上方定义的变量"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
selectedMcp() {
|
||||
return this.mcpList.find(mcp => mcp.mcpId === this.node.mcpParams?.mcpId);
|
||||
return this.mcpList.find((mcp) => mcp.mcpId === this.node.mcpParams?.mcpId);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
@@ -148,7 +148,7 @@ export default {
|
||||
}
|
||||
|
||||
await this.fetchMcpList();
|
||||
|
||||
|
||||
// 确保输出参数有默认值
|
||||
if (!this.outputParams.length) {
|
||||
this.outputParams.push({
|
||||
@@ -180,4 +180,4 @@ export default {
|
||||
.w-full {
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,302 +1,267 @@
|
||||
<template>
|
||||
<div class="panel-content">
|
||||
<!-- 变量列表区域 -->
|
||||
<div class="panel-section">
|
||||
<div class="flex justify-between items-center panel-header">
|
||||
<span>变量列表</span>
|
||||
<el-button type="primary" size="small" @click="addOutput">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<!-- 变量列表区域 -->
|
||||
<div class="panel-section">
|
||||
<div class="flex justify-between items-center panel-header">
|
||||
<span>变量列表</span>
|
||||
<el-button type="primary" size="small" @click="addOutput">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="params-list">
|
||||
<div v-for="(param, index) in inputParams" :key="index" class="mb-2">
|
||||
<div class="param-item">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<div>
|
||||
<el-tag type="primary" size="small">
|
||||
{{ getInputTypeLabel(param.inputType) }}
|
||||
</el-tag>
|
||||
<el-text>
|
||||
|
|
||||
</el-text>
|
||||
<el-tag :type="param.required ? 'danger' : 'info'" size="small">
|
||||
{{ param.required ? '必填' : '选填' }}
|
||||
</el-tag>
|
||||
<el-text>
|
||||
|
|
||||
</el-text>
|
||||
<el-text>
|
||||
{{ param.name }} ({{ param.type }})
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!param.disabled" class="flex gap-2">
|
||||
<el-button type="primary" size="small" @click="editParam(index)">
|
||||
<el-icon>
|
||||
<Edit/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" @click="removeOutput(index)">
|
||||
<el-icon>
|
||||
<Delete/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="params-list">
|
||||
<div v-for="(param, index) in inputParams" :key="index" class="mb-2">
|
||||
<div class="param-item">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<div>
|
||||
<el-tag type="primary" size="small">
|
||||
{{ getInputTypeLabel(param.inputType) }}
|
||||
</el-tag>
|
||||
<el-text> | </el-text>
|
||||
<el-tag :type="param.required ? 'danger' : 'info'" size="small">
|
||||
{{ param.required ? '必填' : '选填' }}
|
||||
</el-tag>
|
||||
<el-text> | </el-text>
|
||||
<el-text> {{ param.name }} ({{ param.type }}) </el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!param.disabled" class="flex gap-2">
|
||||
<el-button type="primary" size="small" @click="editParam(index)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" @click="removeOutput(index)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 变量编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
:title="isEdit ? '编辑变量' : '添加变量'"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="paramForm"
|
||||
:model="editingParam"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item label="显示名称" prop="name">
|
||||
<el-input v-model="editingParam.name" placeholder="请输入显示名称"/>
|
||||
</el-form-item>
|
||||
<!-- 变量编辑对话框 -->
|
||||
<el-dialog v-model="editDialogVisible" :title="isEdit ? '编辑变量' : '添加变量'" width="600px" destroy-on-close>
|
||||
<el-form ref="paramForm" :model="editingParam" :rules="rules" label-position="top">
|
||||
<el-form-item label="显示名称" prop="name">
|
||||
<el-input v-model="editingParam.name" placeholder="请输入显示名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="变量名" prop="type">
|
||||
<el-input v-model="editingParam.type" placeholder="请输入变量名"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="变量名" prop="type">
|
||||
<el-input v-model="editingParam.type" placeholder="请输入变量名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="输入类型" prop="inputType">
|
||||
<el-select
|
||||
v-model="editingParam.inputType"
|
||||
class="w-full"
|
||||
@change="handleEditInputTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in inputTypeDict"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="输入类型" prop="inputType">
|
||||
<el-select v-model="editingParam.inputType" class="w-full" @change="handleEditInputTypeChange">
|
||||
<el-option v-for="item in inputTypeDict" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否必填" prop="required">
|
||||
<el-select v-model="editingParam.required" class="w-full">
|
||||
<el-option :value="false" label="否"/>
|
||||
<el-option :value="true" label="是"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必填" prop="required">
|
||||
<el-select v-model="editingParam.required" class="w-full">
|
||||
<el-option :value="false" label="否" />
|
||||
<el-option :value="true" label="是" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 选项编辑表单 -->
|
||||
<template v-if="editingParam.inputType==='select'">
|
||||
<el-divider content-position="left">选项配置</el-divider>
|
||||
<div class="options-list">
|
||||
<div v-for="(option, index) in editingParam.editingOptions" :key="index" class="mb-2">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="9">
|
||||
<el-form-item
|
||||
:prop="'editingOptions.' + index + '.label'"
|
||||
:rules="{ required: true, message: '请输入选项名称', trigger: 'blur' }"
|
||||
>
|
||||
<el-input v-model="option.label" placeholder="请输入选项名称"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item
|
||||
:prop="'editingOptions.' + index + '.value'"
|
||||
:rules="{ required: true, message: '请输入选项值', trigger: 'blur' }"
|
||||
>
|
||||
<el-input v-model="option.value" placeholder="请输入选项值"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-button @click="removeOption(index)">
|
||||
<el-icon>
|
||||
<Delete/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-button type="primary" @click="addOption">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
添加选项
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
<!-- 选项编辑表单 -->
|
||||
<template v-if="editingParam.inputType === 'select'">
|
||||
<el-divider content-position="left">选项配置</el-divider>
|
||||
<div class="options-list">
|
||||
<div v-for="(option, index) in editingParam.editingOptions" :key="index" class="mb-2">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="9">
|
||||
<el-form-item :prop="'editingOptions.' + index + '.label'" :rules="{ required: true, message: '请输入选项名称', trigger: 'blur' }">
|
||||
<el-input v-model="option.label" placeholder="请输入选项名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :prop="'editingOptions.' + index + '.value'" :rules="{ required: true, message: '请输入选项值', trigger: 'blur' }">
|
||||
<el-input v-model="option.value" placeholder="请输入选项值" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-button @click="removeOption(index)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-button type="primary" @click="addOption">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加选项
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Plus, Delete, Edit} from '@element-plus/icons-vue'
|
||||
import common from './common.ts'
|
||||
import { Plus, Delete, Edit } from '@element-plus/icons-vue';
|
||||
import common from './common.ts';
|
||||
|
||||
export default {
|
||||
name: 'StartPanel',
|
||||
components: {
|
||||
Plus,
|
||||
Delete,
|
||||
Edit
|
||||
},
|
||||
mixins: [common],
|
||||
data() {
|
||||
return {
|
||||
inputTypeDict: [
|
||||
{label: '输入框', value: 'input'},
|
||||
{label: '下拉框', value: 'select'},
|
||||
{label: '数字框', value: 'number'},
|
||||
{label: '文本框', value: 'textarea'},
|
||||
{label: '图片', value: 'image'},
|
||||
],
|
||||
editDialogVisible: false,
|
||||
editingParam: {
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
required: false,
|
||||
inputType: 'input',
|
||||
options: [],
|
||||
editingOptions: []
|
||||
},
|
||||
isEdit: false,
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入显示名称', trigger: 'blur'},
|
||||
{min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur'}
|
||||
],
|
||||
type: [
|
||||
{required: true, message: '请输入变量名', trigger: 'blur'},
|
||||
{
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
|
||||
message: '变量名只能包含字母、数字和下划线,且必须以字母开头',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
inputType: [
|
||||
{required: true, message: '请选择输入类型', trigger: 'change'}
|
||||
],
|
||||
required: [
|
||||
{required: true, message: '请选择是否必填', trigger: 'change'}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateOutputParams()
|
||||
},
|
||||
methods: {
|
||||
addOutput() {
|
||||
this.isEdit = false
|
||||
this.editingParam = {
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
required: false,
|
||||
inputType: 'input',
|
||||
options: [],
|
||||
editingOptions: []
|
||||
}
|
||||
this.editDialogVisible = true
|
||||
},
|
||||
editParam(index) {
|
||||
this.isEdit = true
|
||||
this.editingParamIndex = index
|
||||
const param = {...this.inputParams[index]}
|
||||
param.editingOptions = [...(param.options || [])].map(opt => ({...opt}))
|
||||
this.editingParam = param
|
||||
this.editDialogVisible = true
|
||||
},
|
||||
saveParam() {
|
||||
if (this.isEdit) {
|
||||
this.inputParams[this.editingParamIndex] = {...this.editingParam}
|
||||
} else {
|
||||
this.inputParams.push({...this.editingParam})
|
||||
}
|
||||
this.editDialogVisible = false
|
||||
},
|
||||
handleEditInputTypeChange() {
|
||||
if (this.editingParam.inputType === 'select' && (!this.editingParam.options || this.editingParam.options.length === 0)) {
|
||||
this.editingParam.options = []
|
||||
}
|
||||
},
|
||||
removeOutput(index) {
|
||||
this.inputParams.splice(index, 1)
|
||||
},
|
||||
updateOutputParams() {
|
||||
const outputParams = this.inputParams.map(param => ({
|
||||
name: param.type || '',
|
||||
type: param.type || '',
|
||||
value: '',
|
||||
required: param.required || false,
|
||||
inputType: param.inputType || 'input',
|
||||
options: param.options || []
|
||||
}))
|
||||
name: 'StartPanel',
|
||||
components: {
|
||||
Plus,
|
||||
Delete,
|
||||
Edit,
|
||||
},
|
||||
mixins: [common],
|
||||
data() {
|
||||
return {
|
||||
inputTypeDict: [
|
||||
{ label: '输入框', value: 'input' },
|
||||
{ label: '下拉框', value: 'select' },
|
||||
{ label: '数字框', value: 'number' },
|
||||
{ label: '文本框', value: 'textarea' },
|
||||
{ label: '图片', value: 'image' },
|
||||
],
|
||||
editDialogVisible: false,
|
||||
editingParam: {
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
required: false,
|
||||
inputType: 'input',
|
||||
options: [],
|
||||
editingOptions: [],
|
||||
},
|
||||
isEdit: false,
|
||||
rules: {
|
||||
name: [
|
||||
{ required: true, message: '请输入显示名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请输入变量名', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
|
||||
message: '变量名只能包含字母、数字和下划线,且必须以字母开头',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
inputType: [{ required: true, message: '请选择输入类型', trigger: 'change' }],
|
||||
required: [{ required: true, message: '请选择是否必填', trigger: 'change' }],
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateOutputParams();
|
||||
},
|
||||
methods: {
|
||||
addOutput() {
|
||||
this.isEdit = false;
|
||||
this.editingParam = {
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
required: false,
|
||||
inputType: 'input',
|
||||
options: [],
|
||||
editingOptions: [],
|
||||
};
|
||||
this.editDialogVisible = true;
|
||||
},
|
||||
editParam(index) {
|
||||
this.isEdit = true;
|
||||
this.editingParamIndex = index;
|
||||
const param = { ...this.inputParams[index] };
|
||||
param.editingOptions = [...(param.options || [])].map((opt) => ({ ...opt }));
|
||||
this.editingParam = param;
|
||||
this.editDialogVisible = true;
|
||||
},
|
||||
saveParam() {
|
||||
if (this.isEdit) {
|
||||
this.inputParams[this.editingParamIndex] = { ...this.editingParam };
|
||||
} else {
|
||||
this.inputParams.push({ ...this.editingParam });
|
||||
}
|
||||
this.editDialogVisible = false;
|
||||
},
|
||||
handleEditInputTypeChange() {
|
||||
if (this.editingParam.inputType === 'select' && (!this.editingParam.options || this.editingParam.options.length === 0)) {
|
||||
this.editingParam.options = [];
|
||||
}
|
||||
},
|
||||
removeOutput(index) {
|
||||
this.inputParams.splice(index, 1);
|
||||
},
|
||||
updateOutputParams() {
|
||||
const outputParams = this.inputParams.map((param) => ({
|
||||
name: param.type || '',
|
||||
type: param.type || '',
|
||||
value: '',
|
||||
required: param.required || false,
|
||||
inputType: param.inputType || 'input',
|
||||
options: param.options || [],
|
||||
}));
|
||||
|
||||
if (this.node) {
|
||||
this.node.outputParams = outputParams
|
||||
}
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.paramForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (this.editingParam.inputType === 'select') {
|
||||
this.editingParam.options = [...this.editingParam.editingOptions]
|
||||
}
|
||||
if (this.node) {
|
||||
this.node.outputParams = outputParams;
|
||||
}
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.paramForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (this.editingParam.inputType === 'select') {
|
||||
this.editingParam.options = [...this.editingParam.editingOptions];
|
||||
}
|
||||
|
||||
if (this.isEdit) {
|
||||
this.inputParams[this.editingParamIndex] = {...this.editingParam}
|
||||
} else {
|
||||
this.inputParams.push({...this.editingParam})
|
||||
}
|
||||
this.editDialogVisible = false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
addOption() {
|
||||
this.editingParam.editingOptions.push({
|
||||
label: '',
|
||||
value: ''
|
||||
})
|
||||
},
|
||||
removeOption(index) {
|
||||
this.editingParam.editingOptions.splice(index, 1)
|
||||
},
|
||||
getInputTypeLabel(type) {
|
||||
const found = this.inputTypeDict.find(item => item.value === type)
|
||||
return found ? found.label : '输入框'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
inputParams: {
|
||||
handler() {
|
||||
this.updateOutputParams()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isEdit) {
|
||||
this.inputParams[this.editingParamIndex] = { ...this.editingParam };
|
||||
} else {
|
||||
this.inputParams.push({ ...this.editingParam });
|
||||
}
|
||||
this.editDialogVisible = false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
addOption() {
|
||||
this.editingParam.editingOptions.push({
|
||||
label: '',
|
||||
value: '',
|
||||
});
|
||||
},
|
||||
removeOption(index) {
|
||||
this.editingParam.editingOptions.splice(index, 1);
|
||||
},
|
||||
getInputTypeLabel(type) {
|
||||
const found = this.inputTypeDict.find((item) => item.value === type);
|
||||
return found ? found.label : '输入框';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inputParams: {
|
||||
handler() {
|
||||
this.updateOutputParams();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -55,12 +55,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="text-tips">
|
||||
<el-alert
|
||||
title="提示:此节点将返回您设置的固定文本内容,可用于流程中的固定消息、说明文字等场景。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert title="提示:此节点将返回您设置的固定文本内容,可用于流程中的固定消息、说明文字等场景。" type="info" :closable="false" show-icon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,4 +131,4 @@ export default {
|
||||
.param-item.readonly .el-input {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -8,140 +8,140 @@ import { InputParams, Params } from './utils';
|
||||
* 节点执行结果接口
|
||||
*/
|
||||
export interface NodeExecutionResponse {
|
||||
nodeId: string;
|
||||
type: string;
|
||||
result: any;
|
||||
timestamp: number;
|
||||
tokens?: number;
|
||||
nodeId: string;
|
||||
type: string;
|
||||
result: any;
|
||||
timestamp: number;
|
||||
tokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码节点参数接口
|
||||
*/
|
||||
export interface CodeNodeParams {
|
||||
code: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库节点参数接口
|
||||
*/
|
||||
export interface DbNodeParams {
|
||||
dbId: string | number;
|
||||
sql: string;
|
||||
dbId: string | number;
|
||||
sql: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP节点参数接口
|
||||
*/
|
||||
export interface HttpNodeParams {
|
||||
url: string;
|
||||
method: string;
|
||||
contentType: string;
|
||||
jsonBody?: string;
|
||||
headerParams?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
bodyParams?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
paramsParams?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
url: string;
|
||||
method: string;
|
||||
contentType: string;
|
||||
jsonBody?: string;
|
||||
headerParams?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
bodyParams?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
paramsParams?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM节点参数接口
|
||||
*/
|
||||
export interface LLMNodeParams {
|
||||
modelConfig: Record<string, any>;
|
||||
messages: Array<{
|
||||
role: string;
|
||||
content: string;
|
||||
}>;
|
||||
modelConfig: Record<string, any>;
|
||||
messages: Array<{
|
||||
role: string;
|
||||
content: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知节点参数接口
|
||||
*/
|
||||
export interface NoticeNodeParams {
|
||||
templateCode: string;
|
||||
templateCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 问题节点参数接口
|
||||
*/
|
||||
export interface QuestionNodeParams {
|
||||
question: string;
|
||||
question: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch节点参数接口
|
||||
*/
|
||||
export interface SwitchNodeParams {
|
||||
cases: Array<{
|
||||
value: any;
|
||||
}>;
|
||||
cases: Array<{
|
||||
value: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码节点接口
|
||||
*/
|
||||
export interface CodeNode extends Node {
|
||||
codeParams: CodeNodeParams;
|
||||
codeParams: CodeNodeParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库节点接口
|
||||
*/
|
||||
export interface DbNode extends Node {
|
||||
dbParams: DbNodeParams;
|
||||
dbParams: DbNodeParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP节点接口
|
||||
*/
|
||||
export interface HttpNode extends Node {
|
||||
httpParams: HttpNodeParams;
|
||||
httpParams: HttpNodeParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM节点接口
|
||||
*/
|
||||
export interface LLMNode extends Node {
|
||||
llmParams: LLMNodeParams;
|
||||
llmParams: LLMNodeParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知节点接口
|
||||
*/
|
||||
export interface NoticeNode extends Node {
|
||||
noticeParams: NoticeNodeParams;
|
||||
noticeParams: NoticeNodeParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 问题节点接口
|
||||
*/
|
||||
export interface QuestionNode extends Node {
|
||||
questionParams: QuestionNodeParams;
|
||||
questionParams: QuestionNodeParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch节点接口
|
||||
*/
|
||||
export interface SwitchNode extends Node {
|
||||
switchParams: SwitchNodeParams;
|
||||
code?: string;
|
||||
switchParams: SwitchNodeParams;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行器函数类型
|
||||
*/
|
||||
export type ExecutorFunction<T extends Node> = (
|
||||
node: T,
|
||||
inputParams: InputParams,
|
||||
nodes: Node[],
|
||||
options?: ExecutionOptions
|
||||
) => Promise<NodeExecutionResponse>;
|
||||
node: T,
|
||||
inputParams: InputParams,
|
||||
nodes: Node[],
|
||||
options?: ExecutionOptions
|
||||
) => Promise<NodeExecutionResponse>;
|
||||
|
||||
@@ -4,24 +4,24 @@ import { Node, Connection, ParamItem } from './node';
|
||||
|
||||
// Panel component interface
|
||||
export interface PanelComponent {
|
||||
parent: {
|
||||
nodes: Node[];
|
||||
connections: Connection[];
|
||||
env?: Array<{ name: string; value: any }>;
|
||||
};
|
||||
node: Node;
|
||||
$emit: (event: string, ...args: any[]) => void;
|
||||
parent: {
|
||||
nodes: Node[];
|
||||
connections: Connection[];
|
||||
env?: Array<{ name: string; value: any }>;
|
||||
};
|
||||
node: Node;
|
||||
$emit: (event: string, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
// Node with params
|
||||
export interface NodeWithParams extends Node {
|
||||
inputParams: ParamItem[];
|
||||
outputParams: ParamItem[];
|
||||
inputParams: ParamItem[];
|
||||
outputParams: ParamItem[];
|
||||
}
|
||||
|
||||
// Previous node output structure
|
||||
export interface PreviousNodeOutput {
|
||||
id: string;
|
||||
name: string;
|
||||
list: ParamItem[];
|
||||
}
|
||||
id: string;
|
||||
name: string;
|
||||
list: ParamItem[];
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { FlowClass } from '../types/utils';
|
||||
|
||||
export default class Flow implements FlowClass {
|
||||
/**
|
||||
* 获取统一的URL参数
|
||||
* @param type - 流程类型
|
||||
* @param id - 流程ID
|
||||
* @returns 格式化的URL字符串
|
||||
*/
|
||||
static runUrl(type: string, id: string): string {
|
||||
return `/flow/${type}/${id}`;
|
||||
}
|
||||
/**
|
||||
* 获取统一的URL参数
|
||||
* @param type - 流程类型
|
||||
* @param id - 流程ID
|
||||
* @returns 格式化的URL字符串
|
||||
*/
|
||||
static runUrl(type: string, id: string): string {
|
||||
return `/flow/${type}/${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取icon地址
|
||||
* @param icon - 图标路径
|
||||
* @returns 图标路径或默认图标
|
||||
*/
|
||||
static getIcon(icon?: string): string {
|
||||
return icon ? icon : '/img/chat/icon.png';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取icon地址
|
||||
* @param icon - 图标路径
|
||||
* @returns 图标路径或默认图标
|
||||
*/
|
||||
static getIcon(icon?: string): string {
|
||||
return icon ? icon : '/img/chat/icon.png';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
>
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
</el-button>
|
||||
|
||||
|
||||
<div class="flex-grow ml-4"></div>
|
||||
<div class="flex items-center text-xs text-gray-500 dark:text-gray-400">
|
||||
<el-icon class="mr-1"><Clock /></el-icon>
|
||||
@@ -158,7 +158,7 @@
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
|
||||
|
||||
<!-- 工具抽屉 -->
|
||||
<tools-drawer ref="toolsDrawerRef" />
|
||||
</div>
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="MCP 工具能力" size="40%" direction="rtl" :before-close="handleClose">
|
||||
<!-- 工具列表 -->
|
||||
<div v-loading="loading">
|
||||
<div v-if="toolsList.length === 0 && !loading" class="text-center py-12">
|
||||
<el-empty description="暂无工具数据" />
|
||||
</div>
|
||||
<!-- 工具列表 -->
|
||||
<div v-loading="loading">
|
||||
<div v-if="toolsList.length === 0 && !loading" class="text-center py-12">
|
||||
<el-empty description="暂无工具数据" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="(tool, index) in toolsList"
|
||||
:key="index"
|
||||
class="p-4 border border-gray-200 rounded-lg transition-all duration-200 hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">
|
||||
{{ tool.name || '未命名工具' }}
|
||||
</h4>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ tool.description || '暂无描述' }}
|
||||
</p>
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="(tool, index) in toolsList"
|
||||
:key="index"
|
||||
class="p-4 border border-gray-200 rounded-lg transition-all duration-200 hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white">
|
||||
{{ tool.name || '未命名工具' }}
|
||||
</h4>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ tool.description || '暂无描述' }}
|
||||
</p>
|
||||
</div>
|
||||
<el-tag size="small" type="primary">工具</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 工具详细信息 -->
|
||||
<div class="space-y-3">
|
||||
<!-- 输入参数 -->
|
||||
<div v-if="tool.inputSchema">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输入参数:</h5>
|
||||
<div class="p-3 bg-gray-50 rounded dark:bg-gray-700">
|
||||
<pre class="text-xs text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{{ formatSchema(tool.inputSchema) }}</pre>
|
||||
</div>
|
||||
<el-tag size="small" type="primary">工具</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 工具详细信息 -->
|
||||
<div class="space-y-3">
|
||||
<!-- 输入参数 -->
|
||||
<div v-if="tool.inputSchema">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输入参数:</h5>
|
||||
<div class="p-3 bg-gray-50 rounded dark:bg-gray-700">
|
||||
<pre class="text-xs text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{{ formatSchema(tool.inputSchema) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出参数 -->
|
||||
<div v-if="tool.outputSchema">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输出参数:</h5>
|
||||
<div class="p-3 bg-gray-50 rounded dark:bg-gray-700">
|
||||
<pre class="text-xs text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{{ formatSchema(tool.outputSchema) }}</pre>
|
||||
</div>
|
||||
<!-- 输出参数 -->
|
||||
<div v-if="tool.outputSchema">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输出参数:</h5>
|
||||
<div class="p-3 bg-gray-50 rounded dark:bg-gray-700">
|
||||
<pre class="text-xs text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{{ formatSchema(tool.outputSchema) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
@@ -107,7 +107,7 @@ const formatSchema = (schema: any) => {
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
toolsList.value = [];
|
||||
Object.keys(mcpInfo).forEach(key => delete mcpInfo[key]);
|
||||
Object.keys(mcpInfo).forEach((key) => delete mcpInfo[key]);
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
rows="2"
|
||||
/>
|
||||
<div class="absolute -bottom-5 right-2 text-xs z-10 bg-white/90 px-2 py-1 rounded">
|
||||
<a href="https://cloud.siliconflow.cn/i/YKcJJTYP" target="_blank" class="text-blue-500 hover:text-blue-700 no-underline text-xs">获取【硅基流动】 免费 ApiKey</a>
|
||||
<a href="https://cloud.siliconflow.cn/i/YKcJJTYP" target="_blank" class="text-blue-500 hover:text-blue-700 no-underline text-xs"
|
||||
>获取【硅基流动】 免费 ApiKey</a
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
@@ -28,7 +30,7 @@
|
||||
</el-card>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
@@ -52,14 +54,14 @@ const loading = ref(false);
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
});
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
apiKey: [
|
||||
{ required: true, message: '请输入API Key', trigger: 'blur' },
|
||||
{
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback(new Error('请输入API Key'));
|
||||
@@ -74,86 +76,86 @@ const dataRules = ref({
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 预览要创建的模型
|
||||
const previewModels = ref([
|
||||
{
|
||||
type: 'Chat',
|
||||
typeLabel: '聊天',
|
||||
{
|
||||
type: 'Chat',
|
||||
typeLabel: '聊天',
|
||||
modelName: 'moonshotai/Kimi-K2-Instruct',
|
||||
name: 'kimi-k2',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Chat',
|
||||
typeLabel: '聊天',
|
||||
{
|
||||
type: 'Chat',
|
||||
typeLabel: '聊天',
|
||||
modelName: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-chat',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Embedding',
|
||||
typeLabel: '向量',
|
||||
{
|
||||
type: 'Embedding',
|
||||
typeLabel: '向量',
|
||||
modelName: 'Qwen/Qwen3-Embedding-8B',
|
||||
name: 'qwen-embedding',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Reranker',
|
||||
typeLabel: '排序',
|
||||
{
|
||||
type: 'Reranker',
|
||||
typeLabel: '排序',
|
||||
modelName: 'Qwen/Qwen3-Reranker-8B',
|
||||
name: 'qwen-reranker',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Image',
|
||||
typeLabel: '图片',
|
||||
{
|
||||
type: 'Image',
|
||||
typeLabel: '图片',
|
||||
modelName: 'Kwai-Kolors/Kolors',
|
||||
name: 'kolors-image',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Reason',
|
||||
typeLabel: '推理',
|
||||
{
|
||||
type: 'Reason',
|
||||
typeLabel: '推理',
|
||||
modelName: 'MiniMaxAI/MiniMax-M1-80k',
|
||||
name: 'minimax-reason',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Vision',
|
||||
typeLabel: '视觉',
|
||||
{
|
||||
type: 'Vision',
|
||||
typeLabel: '视觉',
|
||||
modelName: 'Qwen/Qwen2.5-VL-72B-Instruct',
|
||||
name: 'qwen-vision',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Video',
|
||||
typeLabel: '视频',
|
||||
{
|
||||
type: 'Video',
|
||||
typeLabel: '视频',
|
||||
modelName: 'Wan-AI/Wan2.1-I2V-14B-720P',
|
||||
name: 'wan-video',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
{
|
||||
type: 'Voice',
|
||||
typeLabel: '音频',
|
||||
{
|
||||
type: 'Voice',
|
||||
typeLabel: '音频',
|
||||
modelName: 'RVC-Boss/GPT-SoVITS',
|
||||
name: 'gpt-sovits',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
}
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
},
|
||||
]);
|
||||
|
||||
// 检查模型名称是否存在
|
||||
@@ -169,7 +171,7 @@ const checkModelExists = async (name: string): Promise<boolean> => {
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
visible.value = true;
|
||||
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
@@ -184,7 +186,7 @@ const onSubmit = async () => {
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
let skipCount = 0;
|
||||
@@ -215,14 +217,14 @@ const onSubmit = async () => {
|
||||
...(model.type === 'Chat' && {
|
||||
responseLimit: 2048,
|
||||
temperature: 0.4,
|
||||
topP: 0.7
|
||||
topP: 0.7,
|
||||
}),
|
||||
// 为Image类型设置默认参数
|
||||
...(model.type === 'Image' && {
|
||||
imageSize: '1024x1024',
|
||||
imageQuality: 'standard',
|
||||
imageStyle: 'natural'
|
||||
})
|
||||
imageStyle: 'natural',
|
||||
}),
|
||||
};
|
||||
|
||||
await addObj(modelData);
|
||||
@@ -248,17 +250,17 @@ const onSubmit = async () => {
|
||||
if (resultMessages.length > 0) {
|
||||
useMessage().success(resultMessages.join(','));
|
||||
}
|
||||
|
||||
|
||||
// 显示跳过的模型信息
|
||||
if (skipped.length > 0 && skipped.length <= 3) {
|
||||
skipped.forEach(msg => useMessage().warning(msg));
|
||||
skipped.forEach((msg) => useMessage().warning(msg));
|
||||
} else if (skipped.length > 3) {
|
||||
useMessage().warning(`共跳过${skipCount}个已存在的模型`);
|
||||
}
|
||||
|
||||
|
||||
// 显示错误信息
|
||||
if (errors.length > 0 && errors.length <= 3) {
|
||||
errors.forEach(error => useMessage().error(error));
|
||||
errors.forEach((error) => useMessage().error(error));
|
||||
} else if (errors.length > 3) {
|
||||
useMessage().error(`批量创建完成,有${failedCount}个模型创建失败`);
|
||||
}
|
||||
@@ -278,4 +280,4 @@ const onSubmit = async () => {
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item prop="extData">
|
||||
@@ -204,11 +204,9 @@ const availableDimensions = computed(() => {
|
||||
if (form.modelType !== 'Embedding' || !form.modelName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentModel = availableModels.value.find(model =>
|
||||
model.model === form.modelName && model.type === 'Embedding'
|
||||
);
|
||||
|
||||
|
||||
const currentModel = availableModels.value.find((model) => model.model === form.modelName && model.type === 'Embedding');
|
||||
|
||||
return currentModel?.dimensions || [];
|
||||
});
|
||||
|
||||
@@ -259,7 +257,7 @@ const dataRules = ref({
|
||||
],
|
||||
imageSize: [{ required: true, message: '请选择图片大小', trigger: 'change' }],
|
||||
dimensions: [
|
||||
{
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
// 只有当显示维度下拉框时才验证必填
|
||||
if (form.modelType === 'Embedding' && availableDimensions.value.length > 0) {
|
||||
@@ -269,9 +267,9 @@ const dataRules = ref({
|
||||
}
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
extData: [{ validator: rule.json, trigger: 'blur' }],
|
||||
// ... 其他字段的验证规则 ...
|
||||
@@ -374,9 +372,7 @@ watch(
|
||||
() => form.modelName,
|
||||
() => {
|
||||
if (form.modelType === 'Embedding') {
|
||||
const currentModel = availableModels.value.find(model =>
|
||||
model.model === form.modelName && model.type === 'Embedding'
|
||||
);
|
||||
const currentModel = availableModels.value.find((model) => model.model === form.modelName && model.type === 'Embedding');
|
||||
if (currentModel?.dimensions && currentModel.dimensions.length > 0) {
|
||||
form.dimensions = currentModel.dimensions[0].toString();
|
||||
} else {
|
||||
@@ -387,6 +383,4 @@ watch(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
|
||||
|
||||
<!-- 批量新增 -->
|
||||
<batch-form-dialog ref="batchFormDialogRef" @refresh="getDataList(false)" />
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ const dataRules = {
|
||||
prompt: [{ required: true, message: '请输入提示词', trigger: 'blur' }],
|
||||
promptSort: [
|
||||
{ required: true, message: '请输入列表排序', trigger: 'blur' },
|
||||
{ type: 'number', message: '列表排序必须为数字', trigger: 'blur' }
|
||||
{ type: 'number', message: '列表排序必须为数字', trigger: 'blur' },
|
||||
],
|
||||
promptStatus: [{ required: true, message: '请选择是否有效', trigger: 'change' }],
|
||||
};
|
||||
@@ -109,7 +109,7 @@ function getAiPromptData(id: string) {
|
||||
const data = res.data;
|
||||
Object.assign(form, {
|
||||
...data,
|
||||
promptSort: Number(data.promptSort)
|
||||
promptSort: Number(data.promptSort),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -69,7 +69,7 @@ import 'splitpanes/dist/splitpanes.css';
|
||||
import ControlPanel from './components/ControlPanel.vue';
|
||||
import DataDisplay from './components/DataDisplay.vue';
|
||||
import { putObj, getObj, parseDoc } from '/@/api/knowledge/aiReportConf';
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
import VueOfficeDocx from '@vue-office/docx/lib/v3/vue-office-docx.mjs';
|
||||
import '@vue-office/docx/lib/v3/index.css';
|
||||
import other from '/@/utils/other';
|
||||
|
||||
@@ -1,139 +1,125 @@
|
||||
<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>
|
||||
<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">
|
||||
@@ -147,25 +133,25 @@ 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
|
||||
}
|
||||
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)
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const documentTitle = ref('文档查看');
|
||||
@@ -191,272 +177,283 @@ 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;
|
||||
}
|
||||
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);
|
||||
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();
|
||||
}
|
||||
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
|
||||
}
|
||||
loadTriggerRef,
|
||||
([{ isIntersecting }]) => {
|
||||
if (isIntersecting && !loading.value && !noMoreData.value && visible.value && slices.value.length > 0) {
|
||||
loadMoreSlices();
|
||||
}
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
}
|
||||
);
|
||||
|
||||
// 组件卸载时停止观察
|
||||
onBeforeUnmount(() => {
|
||||
stopObserver();
|
||||
stopObserver();
|
||||
});
|
||||
|
||||
// 选择切片
|
||||
const selectSlice = (slice: any) => {
|
||||
if (selectedSliceId.value === slice.id) {
|
||||
selectedSliceId.value = null;
|
||||
selectedSlice.value = null;
|
||||
} else {
|
||||
selectedSliceId.value = slice.id;
|
||||
selectedSlice.value = slice;
|
||||
}
|
||||
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);
|
||||
});
|
||||
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 === slice.content) {
|
||||
editingSlice.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 内容为空
|
||||
if (!editingContent.value.trim()) {
|
||||
useMessage().error('内容不能为空');
|
||||
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;
|
||||
}
|
||||
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 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);
|
||||
}
|
||||
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' });
|
||||
}
|
||||
});
|
||||
// 如果没有找到切片,可能需要加载更多数据
|
||||
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 = [];
|
||||
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(
|
||||
() => visible.value,
|
||||
(newVal) => {
|
||||
if (newVal && props.documentId) {
|
||||
resetPagination();
|
||||
getDocumentSlices();
|
||||
|
||||
watch(() => props.documentId, (newVal) => {
|
||||
if (newVal && visible.value) {
|
||||
resetPagination();
|
||||
getDocumentSlices();
|
||||
// 重置图片错误状态
|
||||
imageError.value = '';
|
||||
}
|
||||
});
|
||||
if (props.sliceId) {
|
||||
selectedSliceId.value = props.sliceId;
|
||||
scrollToSlice(props.sliceId);
|
||||
}
|
||||
} else if (!newVal) {
|
||||
// 关闭抽屉时重置状态
|
||||
activeTab.value = 'document';
|
||||
imageError.value = '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(() => props.sliceId, (newVal) => {
|
||||
if (newVal && visible.value) {
|
||||
selectedSliceId.value = newVal;
|
||||
scrollToSlice(newVal);
|
||||
}
|
||||
});
|
||||
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>
|
||||
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>
|
||||
|
||||
@@ -1,109 +1,107 @@
|
||||
<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>
|
||||
<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';
|
||||
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 visible = ref(false);
|
||||
const loading = ref(false);
|
||||
// 定义字典
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
units: '',
|
||||
fileSize: '',
|
||||
hitCount: '',
|
||||
charCount: '',
|
||||
content: '',
|
||||
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',
|
||||
}]
|
||||
})
|
||||
content: [
|
||||
{ required: true, message: '内容不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 100,
|
||||
max: 1500,
|
||||
message: '文本长度在 100 - 1500 之间',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (id: string) => {
|
||||
visible.value = true
|
||||
form.id = ''
|
||||
visible.value = true;
|
||||
form.id = '';
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
|
||||
// 获取aiSlice信息
|
||||
if (id) {
|
||||
form.id = id
|
||||
getaiSliceData(id)
|
||||
}
|
||||
// 获取aiSlice信息
|
||||
if (id) {
|
||||
form.id = id;
|
||||
getaiSliceData(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => {
|
||||
});
|
||||
if (!valid) return false;
|
||||
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;
|
||||
}
|
||||
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
|
||||
})
|
||||
// 获取数据
|
||||
loading.value = true;
|
||||
getObj(id)
|
||||
.then((res: any) => {
|
||||
Object.assign(form, res.data);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -269,7 +269,7 @@ const saveContent = async (slice: any) => {
|
||||
try {
|
||||
await putObj({
|
||||
...slice,
|
||||
content: editingContent.value
|
||||
content: editingContent.value,
|
||||
});
|
||||
useMessage().success('修改成功');
|
||||
getDataList();
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div
|
||||
v-show="isShow"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
:style="`top: ${y}px; left: ${x}px;`"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.text"
|
||||
class="el-dropdown-menu__item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" :style="{ left: '10px' }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div v-show="isShow" class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu" :style="`top: ${y}px; left: ${x}px;`">
|
||||
<ul class="el-dropdown-menu">
|
||||
<li v-for="item in menuItems" :key="item.text" class="el-dropdown-menu__item" @click="handleItemClick(item)">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" :style="{ left: '10px' }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
x: Number,
|
||||
y: Number,
|
||||
menuItems: Array,
|
||||
x: Number,
|
||||
y: Number,
|
||||
menuItems: Array,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['itemClick', 'close']);
|
||||
@@ -35,16 +26,16 @@ const emit = defineEmits(['itemClick', 'close']);
|
||||
const isShow = ref(false);
|
||||
|
||||
const handleItemClick = (item) => {
|
||||
emit('itemClick', item);
|
||||
isShow.value = false;
|
||||
emit('itemClick', item);
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
isShow.value = true;
|
||||
isShow.value = true;
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
isShow.value = false;
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
defineExpose({ show, hide });
|
||||
@@ -52,28 +43,28 @@ defineExpose({ show, hide });
|
||||
|
||||
<style scoped>
|
||||
.custom-contextmenu {
|
||||
position: fixed;
|
||||
z-index: 2190;
|
||||
min-width: 120px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
z-index: 2190;
|
||||
min-width: 120px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
background-color: #f5f7fa;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="标记信息"
|
||||
width="30%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef">
|
||||
<el-form-item prop="name" label="字段名" :label-width="formLabelWidth">
|
||||
<el-input v-model="form.name" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="description" label="字段描述" :label-width="formLabelWidth">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入描述"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="dialogVisible" title="标记信息" width="30%" :close-on-click-modal="false">
|
||||
<el-form :model="form" :rules="rules" ref="formRef">
|
||||
<el-form-item prop="name" label="字段名" :label-width="formLabelWidth">
|
||||
<el-input v-model="form.name" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="description" label="字段描述" :label-width="formLabelWidth">
|
||||
<el-input v-model="form.description" type="textarea" :rows="4" placeholder="请输入描述"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {rule} from "/@/utils/validate";
|
||||
import { rule } from '/@/utils/validate';
|
||||
|
||||
const props = defineProps({
|
||||
initialName: String,
|
||||
initialDescription: String,
|
||||
initialName: String,
|
||||
initialDescription: String,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit', 'close']);
|
||||
@@ -40,45 +30,45 @@ const emit = defineEmits(['submit', 'close']);
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref(null);
|
||||
const form = reactive({
|
||||
name: props.initialName || '',
|
||||
description: props.initialDescription || ''
|
||||
name: props.initialName || '',
|
||||
description: props.initialDescription || '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{required: true, message: '请输入属性名', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'},
|
||||
{validator: rule.validatorLowercase, trigger: 'blur'}
|
||||
],
|
||||
description: [
|
||||
{required: true, message: '请输入描述', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'},
|
||||
]
|
||||
name: [
|
||||
{ required: true, message: '请输入属性名', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ validator: rule.validatorLowercase, trigger: 'blur' },
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: '请输入描述', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
const formLabelWidth = '80px';
|
||||
|
||||
const submitForm = () => {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('submit', {name: form.name, description: form.description});
|
||||
closeDialog();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('submit', { name: form.name, description: form.description });
|
||||
closeDialog();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('close');
|
||||
dialogVisible.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const showDialog = (name, description) => {
|
||||
form.name = name || '';
|
||||
form.description = description || '';
|
||||
dialogVisible.value = true;
|
||||
form.name = name || '';
|
||||
form.description = description || '';
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({showDialog});
|
||||
defineExpose({ showDialog });
|
||||
</script>
|
||||
|
||||
@@ -1,110 +1,108 @@
|
||||
<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="ocrTitle">
|
||||
<el-input v-model="form.ocrTitle" placeholder="请输入标题"/>
|
||||
</el-form-item>
|
||||
<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="ocrTitle">
|
||||
<el-input v-model="form.ocrTitle" placeholder="请输入标题" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="ocrPrompt">
|
||||
<el-input type="textarea" :rows="5" v-model="form.ocrPrompt" placeholder="请输入描述提示词"/>
|
||||
</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>
|
||||
<el-form-item label="描述" prop="ocrPrompt">
|
||||
<el-input type="textarea" :rows="5" v-model="form.ocrPrompt" placeholder="请输入描述提示词" />
|
||||
</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="AiOcrConfDialog">
|
||||
import {useDict} from '/@/hooks/dict';
|
||||
import {useMessage} from "/@/hooks/message";
|
||||
import {getObj, addObj, putObj, validateExist} from '/@/api/knowledge/ocr'
|
||||
import {rule} from '/@/utils/validate';
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
import { useMessage } from '/@/hooks/message';
|
||||
import { getObj, addObj, putObj, validateExist } from '/@/api/knowledge/ocr';
|
||||
import { rule } from '/@/utils/validate';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// 定义变量内容
|
||||
const dataFormRef = ref();
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
// 定义字典
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
ocrTitle: '',
|
||||
ocrPrompt: '',
|
||||
imageResource: '',
|
||||
ocrMarked: '',
|
||||
id: '',
|
||||
ocrTitle: '',
|
||||
ocrPrompt: '',
|
||||
imageResource: '',
|
||||
ocrMarked: '',
|
||||
});
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = {
|
||||
ocrTitle: [
|
||||
{required: true, message: '请输入功能名称', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'}
|
||||
],
|
||||
ocrPrompt: [
|
||||
{required: true, message: '请输入提示词描述', trigger: 'blur'},
|
||||
{validator: rule.overLength, trigger: 'blur'},
|
||||
]
|
||||
ocrTitle: [
|
||||
{ required: true, message: '请输入功能名称', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
ocrPrompt: [
|
||||
{ required: true, message: '请输入提示词描述', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (id: string) => {
|
||||
visible.value = true
|
||||
form.id = ''
|
||||
visible.value = true;
|
||||
form.id = '';
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
|
||||
// 获取aiOcrConf信息
|
||||
if (id) {
|
||||
form.id = id
|
||||
getAiOcrConfData(id)
|
||||
}
|
||||
// 获取aiOcrConf信息
|
||||
if (id) {
|
||||
form.id = id;
|
||||
getAiOcrConfData(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => {
|
||||
});
|
||||
if (!valid) return false;
|
||||
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;
|
||||
}
|
||||
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 getAiOcrConfData = (id: string) => {
|
||||
// 获取数据
|
||||
loading.value = true
|
||||
getObj({id: id}).then((res: any) => {
|
||||
Object.assign(form, res.data[0])
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
// 获取数据
|
||||
loading.value = true;
|
||||
getObj({ id: id })
|
||||
.then((res: any) => {
|
||||
Object.assign(form, res.data[0]);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user