初始化
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
import { Download } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElText } from 'element-plus';
|
||||
|
||||
import confirmFile from '#/assets/ai/workflow/confirm-file.png';
|
||||
// 导入你的图片资源
|
||||
// 请确保路径正确,或者将图片放在 public 目录下引用
|
||||
import confirmOther from '#/assets/ai/workflow/confirm-other.png';
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({
|
||||
// v-model 绑定值
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: null,
|
||||
},
|
||||
// 数据类型: text, image, video, audio, other, file
|
||||
selectionDataType: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
// 数据列表
|
||||
selectionData: {
|
||||
type: Array as () => any[],
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 定义 Emits
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 判断是否选中
|
||||
const isSelected = (item: any) => {
|
||||
return props.modelValue === item;
|
||||
};
|
||||
|
||||
// 切换选中状态
|
||||
const changeValue = (item: any) => {
|
||||
if (props.modelValue === item) {
|
||||
// 如果点击已选中的,则取消选中
|
||||
emit('update:modelValue', null);
|
||||
emit('change', null); // 触发 Element Plus 表单验证
|
||||
} else {
|
||||
emit('update:modelValue', item);
|
||||
emit('change', item); // 触发 Element Plus 表单验证
|
||||
}
|
||||
};
|
||||
|
||||
// 获取图标
|
||||
const getIcon = (type: string) => {
|
||||
return type === 'other' ? confirmOther : confirmFile;
|
||||
};
|
||||
|
||||
// 下载处理
|
||||
const handleDownload = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-radio-group">
|
||||
<template v-for="(item, index) in selectionData" :key="index">
|
||||
<!-- 类型: Text -->
|
||||
<div
|
||||
v-if="selectionDataType === 'text'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
<!-- 类型: Image -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'image'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="padding: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<img
|
||||
:src="item"
|
||||
alt=""
|
||||
style="width: 80px; height: 80px; border-radius: 8px; display: block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Video -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'video'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<video controls :src="item" style="width: 162px; height: 141px"></video>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Audio -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'audio'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<audio
|
||||
controls
|
||||
:src="item"
|
||||
style="width: 100%; height: 44px; margin-top: 8px"
|
||||
></audio>
|
||||
</div>
|
||||
|
||||
<!-- 类型: File 或 Other -->
|
||||
<div
|
||||
v-else-if="
|
||||
selectionDataType === 'other' || selectionDataType === 'file'
|
||||
"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div style="width: 92%; display: flex; align-items: center">
|
||||
<img
|
||||
style="width: 20px; height: 20px; margin-right: 8px"
|
||||
alt=""
|
||||
:src="getIcon(selectionDataType)"
|
||||
/>
|
||||
<!-- 使用 Element Plus 的 Text 组件处理省略号,如果没有安装 Element Plus,可以用普通的 span + css -->
|
||||
<ElText truncated>
|
||||
{{ item }}
|
||||
</ElText>
|
||||
</div>
|
||||
<div class="download-icon-btn" @click.stop="handleDownload(item)">
|
||||
<ElIcon><Download /></ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-radio-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-radio-option {
|
||||
background-color: var(--el-bg-color);
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 1px var(--el-border-color);
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box; /* 确保 padding 不会撑大宽度 */
|
||||
}
|
||||
|
||||
.custom-radio-option:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
|
||||
padding: 8px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 6px 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 7px;
|
||||
width: 9px;
|
||||
height: 4px;
|
||||
border-left: 1px solid white;
|
||||
border-bottom: 1px solid white;
|
||||
transform: rotate(-45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.download-icon-btn {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
display: flex; /* 为了对齐图标 */
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<script setup lang="ts">
|
||||
import { Download } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElText } from 'element-plus';
|
||||
|
||||
import confirmFile from '#/assets/ai/workflow/confirm-file.png';
|
||||
// 导入你的图片资源
|
||||
import confirmOther from '#/assets/ai/workflow/confirm-other.png';
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({
|
||||
// v-model 绑定值,多选版本这里是数组
|
||||
modelValue: {
|
||||
type: Array as () => any[],
|
||||
default: () => [],
|
||||
},
|
||||
// 数据类型: text, image, video, audio, other, file
|
||||
selectionDataType: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
// 数据列表
|
||||
selectionData: {
|
||||
type: Array as () => any[],
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 定义 Emits
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 判断是否选中
|
||||
const isSelected = (item: any) => {
|
||||
return props.modelValue && props.modelValue.includes(item);
|
||||
};
|
||||
|
||||
// 切换选中状态 (多选逻辑)
|
||||
const changeValue = (item: any) => {
|
||||
// 复制一份当前数组,避免直接修改 prop
|
||||
const currentValues = props.modelValue ? [...props.modelValue] : [];
|
||||
|
||||
const index = currentValues.indexOf(item);
|
||||
|
||||
if (index === -1) {
|
||||
// 如果不存在,则添加
|
||||
currentValues.push(item);
|
||||
} else {
|
||||
// 如果已存在,则移除
|
||||
currentValues.splice(index, 1);
|
||||
}
|
||||
|
||||
// 更新 v-model
|
||||
emit('update:modelValue', currentValues);
|
||||
// 触发 Element Plus 表单验证
|
||||
emit('change', currentValues);
|
||||
};
|
||||
|
||||
// 获取图标
|
||||
const getIcon = (type: string) => {
|
||||
return type === 'other' ? confirmOther : confirmFile;
|
||||
};
|
||||
|
||||
// 下载处理
|
||||
const handleDownload = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-radio-group">
|
||||
<template v-for="(item, index) in selectionData" :key="index">
|
||||
<!-- 类型: Text -->
|
||||
<div
|
||||
v-if="selectionDataType === 'text'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
<!-- 类型: Image -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'image'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="padding: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<img
|
||||
:src="item"
|
||||
alt=""
|
||||
style="width: 80px; height: 80px; border-radius: 8px; display: block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Video -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'video'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<video controls :src="item" style="width: 162px; height: 141px"></video>
|
||||
</div>
|
||||
|
||||
<!-- 类型: Audio -->
|
||||
<div
|
||||
v-else-if="selectionDataType === 'audio'"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 300px; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<audio controls :src="item" style="width: 100%; height: 40px"></audio>
|
||||
</div>
|
||||
|
||||
<!-- 类型: File 或 Other -->
|
||||
<div
|
||||
v-else-if="
|
||||
selectionDataType === 'other' || selectionDataType === 'file'
|
||||
"
|
||||
class="custom-radio-option"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
style="width: 100%; flex-shrink: 0"
|
||||
@click="changeValue(item)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div style="width: 92%; display: flex; align-items: center">
|
||||
<img
|
||||
style="width: 20px; height: 20px; margin-right: 8px"
|
||||
alt=""
|
||||
:src="getIcon(selectionDataType)"
|
||||
/>
|
||||
<!-- 使用 Element Plus 的 Text 组件处理省略号 -->
|
||||
<ElText truncated>
|
||||
{{ item }}
|
||||
</ElText>
|
||||
</div>
|
||||
<div class="download-icon-btn" @click.stop="handleDownload(item)">
|
||||
<ElIcon><Download /></ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 这里复用之前的 CSS,样式完全一致 */
|
||||
.custom-radio-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-radio-option {
|
||||
background-color: var(--el-bg-color);
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 1px var(--el-border-color);
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.custom-radio-option:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
|
||||
padding: 8px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 6px 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.custom-radio-option.selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 7px;
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
border-left: 1px solid white;
|
||||
border-bottom: 1px solid white;
|
||||
transform: rotate(-45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.download-icon-btn {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
|
||||
import { ElEmpty, ElMessage, ElRow } from 'element-plus';
|
||||
|
||||
import ShowJson from '#/components/json/ShowJson.vue';
|
||||
import { $t } from '#/locales';
|
||||
import ExecResultItem from '#/views/ai/workflow/components/ExecResultItem.vue';
|
||||
|
||||
export interface ExecResultProps {
|
||||
workflowId: any;
|
||||
nodeJson: any;
|
||||
initSignal?: boolean;
|
||||
pollingData?: any;
|
||||
}
|
||||
const props = defineProps<ExecResultProps>();
|
||||
|
||||
const finalNode = computed(() => {
|
||||
const nodes = props.nodeJson;
|
||||
if (nodes.length > 0) {
|
||||
let endNode = nodes[nodes.length - 1].original;
|
||||
for (const node of nodes) {
|
||||
if (node.original.type === 'endNode') {
|
||||
endNode = node.original;
|
||||
}
|
||||
}
|
||||
return endNode;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const result = ref('');
|
||||
const success = ref(false);
|
||||
watch(
|
||||
() => props.initSignal,
|
||||
() => {
|
||||
result.value = '';
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.pollingData,
|
||||
(newVal) => {
|
||||
if (newVal.status === 20) {
|
||||
ElMessage.success($t('message.success'));
|
||||
result.value = newVal.result;
|
||||
success.value = true;
|
||||
}
|
||||
if (newVal.status === 21) {
|
||||
ElMessage.error($t('message.fail'));
|
||||
result.value = newVal.message;
|
||||
success.value = false;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
function getResultCount(res: any[]) {
|
||||
if (res.length > 1 || finalNode.value.data.outputDefs.length > 1) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function getResult(res: any) {
|
||||
return Array.isArray(res) ? res : [res];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="finalNode.type === 'endNode' && success">
|
||||
<ElRow :gutter="12" v-if="finalNode.data.outputDefs && result">
|
||||
<template
|
||||
v-for="outputDef in finalNode.data.outputDefs"
|
||||
:key="outputDef.id"
|
||||
>
|
||||
<ExecResultItem
|
||||
:result="getResult(result[outputDef.name])"
|
||||
:result-count="getResultCount(getResult(result[outputDef.name]))"
|
||||
:content-type="outputDef.contentType || 'text'"
|
||||
:def-name="outputDef.name"
|
||||
/>
|
||||
</template>
|
||||
</ElRow>
|
||||
</div>
|
||||
<div v-if="finalNode.type !== 'endNode' && !success">
|
||||
<ShowJson :value="result" />
|
||||
</div>
|
||||
<div>
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
v-if="!result"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ElCard, ElCol, ElImage } from 'element-plus';
|
||||
|
||||
import fileIcon from '#/assets/ai/workflow/fileIcon.png';
|
||||
|
||||
const props = defineProps({
|
||||
defName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contentType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resultCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
result: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
function makeItem(item: any, index: number) {
|
||||
const name = `${props.defName}-${index + 1}`;
|
||||
// 保存需要用
|
||||
return {
|
||||
resourceName: name,
|
||||
resourceUrl: item,
|
||||
title: name,
|
||||
filePath: item,
|
||||
content: typeof item === 'string' ? item : JSON.stringify(item),
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCol
|
||||
:span="resultCount === 1 ? 24 : 12"
|
||||
v-for="(item, idx) of result"
|
||||
:key="idx"
|
||||
>
|
||||
<ElCard shadow="hover" class="mb-3">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="font-medium">
|
||||
{{ makeItem(item, idx).resourceName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-40 w-full overflow-auto break-words">
|
||||
<ElImage
|
||||
v-if="contentType === 'image'"
|
||||
:src="`${item}`"
|
||||
:preview-src-list="[`${item}`]"
|
||||
class="h-36 w-full"
|
||||
fit="contain"
|
||||
/>
|
||||
<video
|
||||
v-if="contentType === 'video'"
|
||||
controls
|
||||
:src="`${item}`"
|
||||
class="h-36 w-full"
|
||||
></video>
|
||||
<audio
|
||||
v-if="contentType === 'audio'"
|
||||
controls
|
||||
:src="`${item}`"
|
||||
class="h-3/5 w-full"
|
||||
></audio>
|
||||
<div v-if="contentType === 'text'">
|
||||
{{ typeof item === 'string' ? item : JSON.stringify(item) }}
|
||||
</div>
|
||||
<div v-if="contentType === 'other' || contentType === 'file'">
|
||||
<div class="mt-5 flex justify-center">
|
||||
<img :src="fileIcon" alt="" class="h-20 w-20" />
|
||||
</div>
|
||||
<div class="mt-3 text-center">
|
||||
<a :href="`${item}`" target="_blank">{{ $t('button.download') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import ShowJson from '#/components/json/ShowJson.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
interface Props {
|
||||
workflowId: any;
|
||||
node: any;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const singleRunForm = ref<FormInstance>();
|
||||
const runParams = ref<any>({});
|
||||
const submitLoading = ref(false);
|
||||
const result = ref<any>('');
|
||||
function submit() {
|
||||
singleRunForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const params = {
|
||||
workflowId: props.workflowId,
|
||||
nodeId: props.node.id,
|
||||
variables: runParams.value,
|
||||
};
|
||||
submitLoading.value = true;
|
||||
api.post('/api/v1/workflow/singleRun', params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
result.value = res.data;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ElForm label-position="top" ref="singleRunForm" :model="runParams">
|
||||
<ElFormItem
|
||||
v-for="(item, idx) in node?.data.parameters"
|
||||
:prop="item.name"
|
||||
:key="idx"
|
||||
:label="item.description || item.name"
|
||||
:rules="[{ required: true, message: $t('message.required') }]"
|
||||
>
|
||||
<ElInput
|
||||
v-if="item.formType === 'input' || !item.formType"
|
||||
v-model="runParams[item.name]"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="submit"
|
||||
:loading="submitLoading"
|
||||
:icon="Position"
|
||||
>
|
||||
{{ $t('button.run') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="mb-2.5 mt-2.5 font-semibold">
|
||||
{{ $t('workflow.result') }}:
|
||||
</div>
|
||||
<ShowJson :value="result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElForm, ElFormItem } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import WorkflowFormItem from './WorkflowFormItem.vue';
|
||||
|
||||
export type WorkflowFormProps = {
|
||||
onAsyncExecute?: (values: any) => void;
|
||||
onSubmit?: (values: any) => void;
|
||||
tinyFlowData: any;
|
||||
workflowId: any;
|
||||
workflowParams: any;
|
||||
};
|
||||
const props = withDefaults(defineProps<WorkflowFormProps>(), {
|
||||
onExecuting: () => {
|
||||
console.warn('no execute method');
|
||||
},
|
||||
onSubmit: () => {
|
||||
console.warn('no submit method');
|
||||
},
|
||||
onAsyncExecute: () => {
|
||||
console.warn('no async execute method');
|
||||
},
|
||||
});
|
||||
defineExpose({
|
||||
resume,
|
||||
});
|
||||
const runForm = ref<FormInstance>();
|
||||
const runParams = ref<any>({});
|
||||
const submitLoading = ref(false);
|
||||
const parameters = computed(() => {
|
||||
return props.workflowParams.parameters;
|
||||
});
|
||||
const executeId = ref('');
|
||||
function resume(data: any) {
|
||||
data.executeId = executeId.value;
|
||||
submitLoading.value = true;
|
||||
api.post('/api/v1/workflow/resume', data).then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
startPolling(executeId.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function submitV2() {
|
||||
runForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const data = {
|
||||
id: props.workflowId,
|
||||
variables: {
|
||||
...runParams.value,
|
||||
},
|
||||
};
|
||||
props.onSubmit?.(runParams.value);
|
||||
submitLoading.value = true;
|
||||
api.post('/api/v1/workflow/runAsync', data).then((res) => {
|
||||
if (res.errorCode === 0 && res.data) {
|
||||
// executeId
|
||||
executeId.value = res.data;
|
||||
startPolling(res.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const timer = ref();
|
||||
const nodes = ref(
|
||||
props.tinyFlowData.nodes.map((node: any) => ({
|
||||
nodeId: node.id,
|
||||
nodeName: node.data.title,
|
||||
})),
|
||||
);
|
||||
// 轮询执行结果
|
||||
function startPolling(executeId: any) {
|
||||
if (timer.value) return;
|
||||
timer.value = setInterval(() => executePolling(executeId), 1000);
|
||||
}
|
||||
function executePolling(executeId: any) {
|
||||
api
|
||||
.post('/api/v1/workflow/getChainStatus', {
|
||||
executeId,
|
||||
nodes: nodes.value,
|
||||
})
|
||||
.then((res) => {
|
||||
// 5 是挂起状态
|
||||
if (res.data.status !== 1 || res.data.status === 5) {
|
||||
stopPolling();
|
||||
}
|
||||
props.onAsyncExecute?.(res.data);
|
||||
});
|
||||
}
|
||||
function stopPolling() {
|
||||
submitLoading.value = false;
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
}
|
||||
onUnmounted(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ElForm label-position="top" ref="runForm" :model="runParams">
|
||||
<WorkflowFormItem
|
||||
v-model:run-params="runParams"
|
||||
:parameters="parameters"
|
||||
/>
|
||||
<ElFormItem>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="submitV2"
|
||||
:loading="submitLoading"
|
||||
:icon="Position"
|
||||
>
|
||||
{{ $t('button.run') }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ElAlert,
|
||||
ElCheckboxGroup,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import ChooseResource from '#/views/ai/resource/ChooseResource.vue';
|
||||
|
||||
const props = defineProps({
|
||||
parameters: {
|
||||
type: Array<any>,
|
||||
required: true,
|
||||
},
|
||||
runParams: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
propPrefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:runParams']);
|
||||
function getContentType(item: any) {
|
||||
return item.contentType || 'text';
|
||||
}
|
||||
function isResource(contentType: any) {
|
||||
return ['audio', 'file', 'image', 'video'].includes(contentType);
|
||||
}
|
||||
function getCheckboxOptions(item: any) {
|
||||
if (item.enums) {
|
||||
return (
|
||||
item.enums?.map((option: any) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function updateParam(name: string, value: any) {
|
||||
const newValue = { ...props.runParams, [name]: value };
|
||||
emit('update:runParams', newValue);
|
||||
}
|
||||
function choose(data: any, propName: string) {
|
||||
updateParam(propName, data.resourceUrl);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElFormItem
|
||||
v-for="(item, idx) in parameters"
|
||||
:prop="`${propPrefix}${item.name}`"
|
||||
:key="idx"
|
||||
:label="item.formLabel || item.name"
|
||||
:rules="
|
||||
item.required ? [{ required: true, message: $t('message.required') }] : []
|
||||
"
|
||||
>
|
||||
<template v-if="getContentType(item) === 'text'">
|
||||
<ElInput
|
||||
v-if="item.formType === 'input' || !item.formType"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
<ElSelect
|
||||
v-if="item.formType === 'select'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
:options="getCheckboxOptions(item)"
|
||||
clearable
|
||||
/>
|
||||
<ElInput
|
||||
v-if="item.formType === 'textarea'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
/>
|
||||
<ElRadioGroup
|
||||
v-if="item.formType === 'radio'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:options="getCheckboxOptions(item)"
|
||||
/>
|
||||
<ElCheckboxGroup
|
||||
v-if="item.formType === 'checkbox'"
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:options="getCheckboxOptions(item)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="getContentType(item) === 'other'">
|
||||
<ElInput
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="isResource(getContentType(item))">
|
||||
<ElInput
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
<ChooseResource :attr-name="item.name" @choose="choose" />
|
||||
</template>
|
||||
<ElAlert v-if="item.formDescription" type="info" style="margin-top: 5px">
|
||||
{{ item.formDescription }}
|
||||
</ElAlert>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,273 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
CircleCloseFilled,
|
||||
SuccessFilled,
|
||||
VideoPause,
|
||||
WarningFilled,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
} from 'element-plus';
|
||||
|
||||
import ShowJson from '#/components/json/ShowJson.vue';
|
||||
import { $t } from '#/locales';
|
||||
import ConfirmItem from '#/views/ai/workflow/components/ConfirmItem.vue';
|
||||
import ConfirmItemMulti from '#/views/ai/workflow/components/ConfirmItemMulti.vue';
|
||||
|
||||
export interface WorkflowStepsProps {
|
||||
workflowId: any;
|
||||
nodeJson: any;
|
||||
initSignal?: boolean;
|
||||
pollingData?: any;
|
||||
}
|
||||
const props = defineProps<WorkflowStepsProps>();
|
||||
const emit = defineEmits(['resume']);
|
||||
const nodes = ref<any[]>([]);
|
||||
const nodeStatusMap = ref<Record<string, any>>({});
|
||||
const isChainError = ref(false);
|
||||
watch(
|
||||
() => props.pollingData,
|
||||
(newVal) => {
|
||||
const nodes = newVal.nodes;
|
||||
if (newVal.status === 21) {
|
||||
isChainError.value = true;
|
||||
chainErrMsg.value = newVal.message;
|
||||
}
|
||||
if (![20, 21].includes(newVal.status)) {
|
||||
confirmBtnLoading.value = false;
|
||||
}
|
||||
for (const nodeId in nodes) {
|
||||
nodeStatusMap.value[nodeId] = nodes[nodeId];
|
||||
if (nodes[nodeId].status === 5) {
|
||||
activeName.value = nodeId;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
watch(
|
||||
() => props.initSignal,
|
||||
() => {
|
||||
nodeStatusMap.value = {};
|
||||
isChainError.value = false;
|
||||
confirmBtnLoading.value = false;
|
||||
chainErrMsg.value = '';
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.nodeJson,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
nodes.value = [...newVal];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const displayNodes = computed(() => {
|
||||
return nodes.value.map((node) => ({
|
||||
...node,
|
||||
...nodeStatusMap.value[node.key],
|
||||
}));
|
||||
});
|
||||
const activeName = ref('1');
|
||||
const confirmParams = ref<any>({});
|
||||
// 定义一个对象来存储所有的 form 实例,key 为 node.key
|
||||
const formRefs = ref<Record<string, FormInstance>>({});
|
||||
// 动态设置 Ref 的辅助函数
|
||||
const setFormRef = (el: any, key: string) => {
|
||||
if (el) {
|
||||
formRefs.value[key] = el as FormInstance;
|
||||
}
|
||||
};
|
||||
const confirmBtnLoading = ref(false);
|
||||
const chainErrMsg = ref('');
|
||||
function getSelectMode(ops: any) {
|
||||
return ops.formType || 'radio';
|
||||
}
|
||||
function handleConfirm(node: any) {
|
||||
const nodeKey = node.key;
|
||||
// 根据 key 获取具体的 form 实例
|
||||
const form = formRefs.value[nodeKey];
|
||||
|
||||
if (!form) {
|
||||
console.warn(`Form instance for ${nodeKey} not found`);
|
||||
return;
|
||||
}
|
||||
const confirmKey = node.suspendForParameters[0].name;
|
||||
form.validate((valid) => {
|
||||
if (valid) {
|
||||
const value = {
|
||||
confirmParams: {
|
||||
[confirmKey]: 'yes',
|
||||
...confirmParams.value,
|
||||
},
|
||||
};
|
||||
confirmBtnLoading.value = true;
|
||||
emit('resume', value);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<ElAlert v-if="chainErrMsg" :title="chainErrMsg" type="error" />
|
||||
</div>
|
||||
<ElCollapse v-model="activeName" accordion expand-icon-position="left">
|
||||
<ElCollapseItem
|
||||
v-for="node in displayNodes"
|
||||
:key="node.key"
|
||||
:title="`${node.label}-${node.status}`"
|
||||
:name="node.key"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ElIcon
|
||||
v-if="node.status === 20 && !isChainError"
|
||||
color="green"
|
||||
size="20"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</ElIcon>
|
||||
<div v-if="node.status === 1" class="spinner"></div>
|
||||
<ElIcon
|
||||
v-if="node.status === 21 && !isChainError"
|
||||
color="red"
|
||||
size="20"
|
||||
>
|
||||
<CircleCloseFilled />
|
||||
</ElIcon>
|
||||
<ElIcon
|
||||
v-if="node.status === 5 && !isChainError"
|
||||
color="orange"
|
||||
size="20"
|
||||
>
|
||||
<VideoPause />
|
||||
</ElIcon>
|
||||
<ElIcon v-if="isChainError" color="orange" size="20">
|
||||
<WarningFilled />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="node.original.type === 'confirmNode'" class="p-2.5">
|
||||
<div class="mb-2 text-[16px] font-bold">
|
||||
{{ node.original.data.message }}
|
||||
</div>
|
||||
<ElForm
|
||||
:ref="(el) => setFormRef(el, node.key)"
|
||||
label-position="top"
|
||||
:model="confirmParams"
|
||||
>
|
||||
<template
|
||||
v-for="(ops, idx) in node.suspendForParameters"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="header-container" v-if="ops.formType !== 'confirm'">
|
||||
<div class="blue-bar"> </div>
|
||||
<span>{{ ops.formLabel || $t('message.confirmItem') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="description-container"
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
>
|
||||
{{ ops.formDescription }}
|
||||
</div>
|
||||
<ElFormItem
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
:prop="ops.name"
|
||||
:rules="[{ required: true, message: $t('message.required') }]"
|
||||
>
|
||||
<ConfirmItem
|
||||
v-if="getSelectMode(ops) === 'radio'"
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
<ConfirmItemMulti
|
||||
v-else
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem v-if="node.suspendForParameters?.length > 0">
|
||||
<div class="flex justify-end">
|
||||
<ElButton
|
||||
:disabled="confirmBtnLoading"
|
||||
type="primary"
|
||||
@click="handleConfirm(node)"
|
||||
>
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ShowJson :value="node.result || node.message" />
|
||||
</div>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgb(255 255 255 / 30%);
|
||||
border-top-color: var(--el-color-primary);
|
||||
border-right-color: var(--el-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.blue-bar {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
margin-right: 16px;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.description-container {
|
||||
margin-bottom: 16px;
|
||||
color: #969799;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user