初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">&nbsp;</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>