初始化

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,131 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { sortNodes } from '@easyflow/utils';
import { ArrowLeft } from '@element-plus/icons-vue';
import { ElAvatar, ElButton, ElCard, ElCol, ElRow } from 'element-plus';
import { api } from '#/api/request';
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
import { $t } from '#/locales';
import { router } from '#/router';
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
onMounted(async () => {
pageLoading.value = true;
await Promise.all([getWorkflowInfo(workflowId.value), getRunningParams()]);
pageLoading.value = false;
});
const pageLoading = ref(false);
const route = useRoute();
const workflowId = ref(route.query.id);
const workflowInfo = ref<any>({});
const runParams = ref<any>(null);
const initState = ref(false);
const tinyFlowData = ref<any>(null);
const workflowForm = ref();
async function getWorkflowInfo(workflowId: any) {
api.get(`/api/v1/workflow/detail?id=${workflowId}`).then((res) => {
workflowInfo.value = res.data;
tinyFlowData.value = workflowInfo.value.content
? JSON.parse(workflowInfo.value.content)
: {};
});
}
async function getRunningParams() {
api
.get(`/api/v1/workflow/getRunningParameters?id=${workflowId.value}`)
.then((res) => {
runParams.value = res.data;
});
}
function onSubmit() {
initState.value = !initState.value;
}
function resumeChain(data: any) {
workflowForm.value?.resume(data);
}
const chainInfo = ref<any>(null);
function onAsyncExecute(info: any) {
chainInfo.value = info;
}
</script>
<template>
<div
v-loading="pageLoading"
class="bg-background-deep flex h-full max-h-[calc(100vh-90px)] w-full flex-col gap-6 overflow-hidden p-6"
>
<div>
<ElButton :icon="ArrowLeft" @click="router.back()">
{{ $t('button.back') }}
</ElButton>
</div>
<div
class="flex h-[150px] shrink-0 items-center gap-6 rounded-lg border border-[var(--el-border-color)] bg-[var(--el-bg-color)] pl-11"
>
<ElAvatar
class="shrink-0"
:src="workflowInfo.icon ?? workflowIcon"
:size="72"
/>
<div class="flex flex-col gap-5">
<span class="text-2xl font-medium">{{ workflowInfo.title }}</span>
<span class="text-base text-[#75808d]">{{
workflowInfo.description
}}</span>
</div>
</div>
<ElRow class="h-full overflow-hidden" :gutter="10">
<ElCol :span="10" class="h-full overflow-hidden">
<div class="grid h-full grid-rows-2 gap-2.5">
<ElCard shadow="never" style="height: 100%; overflow: auto">
<div class="mb-2.5 font-semibold">
{{ $t('aiWorkflow.params') }}
</div>
<WorkflowForm
v-if="runParams && tinyFlowData"
ref="workflowForm"
:workflow-id="workflowId"
:workflow-params="runParams"
:on-submit="onSubmit"
:on-async-execute="onAsyncExecute"
:tiny-flow-data="tinyFlowData"
/>
</ElCard>
<ElCard shadow="never" style="height: 100%; overflow: auto">
<div class="mb-2.5 font-semibold">
{{ $t('aiWorkflow.steps') }}
</div>
<WorkflowSteps
v-if="tinyFlowData"
:workflow-id="workflowId"
:node-json="sortNodes(tinyFlowData)"
:init-signal="initState"
:polling-data="chainInfo"
@resume="resumeChain"
/>
</ElCard>
</div>
</ElCol>
<ElCol :span="14">
<ElCard shadow="never" style="height: 100%; overflow: auto">
<div class="mb-2.5 mt-2.5 font-semibold">
{{ $t('aiWorkflow.result') }}
</div>
<ExecResult
v-if="tinyFlowData"
:workflow-id="workflowId"
:node-json="sortNodes(tinyFlowData)"
:init-signal="initState"
:polling-data="chainInfo"
/>
</ElCard>
</ElCol>
</ElRow>
</div>
</template>

View File

@@ -0,0 +1,317 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { getOptions, sortNodes } from '@easyflow/utils';
import { ArrowLeft, Position } from '@element-plus/icons-vue';
import { Tinyflow } from '@tinyflow-ai/vue';
import { ElButton, ElDrawer, ElMessage, ElSkeleton } from 'element-plus';
import { api } from '#/api/request';
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
import { $t } from '#/locales';
import { router } from '#/router';
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
import { getCustomNode } from './customNode/index';
import nodeNames from './customNode/nodeNames';
import '@tinyflow-ai/vue/dist/index.css';
const route = useRoute();
// vue
onMounted(async () => {
document.addEventListener('keydown', handleKeydown);
await Promise.all([
loadCustomNode(),
getLlmList(),
getKnowledgeList(),
getWorkflowInfo(workflowId.value),
]);
showTinyFlow.value = true;
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
});
// variables
const tinyflowRef = ref<InstanceType<typeof Tinyflow> | null>(null);
const workflowId = ref(route.query.id);
const workflowInfo = ref<any>({});
const runParams = ref<any>(null);
const tinyFlowData = ref<any>(null);
const llmList = ref<any>([]);
const knowledgeList = ref<any>([]);
const provider = computed(() => ({
llm: () => getOptions('title', 'id', llmList.value),
knowledge: () => getOptions('title', 'id', knowledgeList.value),
searchEngine: (): any => [
{
value: 'bocha-search',
label: $t('aiWorkflow.bochaSearch'),
},
],
}));
const customNode = ref();
const showTinyFlow = ref(false);
const saveLoading = ref(false);
const handleKeydown = (event: KeyboardEvent) => {
// 检查是否是 Ctrl+S
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault(); // 阻止浏览器默认保存行为
if (!saveLoading.value) {
handleSave(true);
}
}
};
const drawerVisible = ref(false);
const initState = ref(false);
const singleNode = ref<any>();
const singleRunVisible = ref(false);
const workflowForm = ref();
const workflowSelectRef = ref();
const updateWorkflowNode = ref<any>(null);
const pluginSelectRef = ref();
const updatePluginNode = ref<any>(null);
const pageLoading = ref(false);
const chainInfo = ref<any>(null);
// functions
async function loadCustomNode() {
customNode.value = await getCustomNode({
handleChosen: (nodeName: string, updateNodeData: any, value: string) => {
const v = [];
if (value) {
v.push(value);
}
if (nodeName === nodeNames.workflowNode) {
workflowSelectRef.value.openDialog(v);
updateWorkflowNode.value = updateNodeData;
}
if (nodeName === nodeNames.pluginNode) {
pluginSelectRef.value.openDialog(v);
updatePluginNode.value = updateNodeData;
}
},
});
}
async function runWorkflow() {
if (!saveLoading.value) {
await handleSave().then(() => {
getWorkflowInfo(workflowId.value);
getRunningParams();
});
}
}
async function handleSave(showMsg: boolean = false) {
saveLoading.value = true;
await api
.post('/api/v1/workflow/update', {
id: workflowId.value,
content: tinyflowRef.value?.getData(),
})
.then((res) => {
saveLoading.value = false;
if (res.errorCode === 0 && showMsg) {
ElMessage.success(res.message);
}
});
}
async function getWorkflowInfo(workflowId: any) {
api.get(`/api/v1/workflow/detail?id=${workflowId}`).then((res) => {
workflowInfo.value = res.data;
tinyFlowData.value = workflowInfo.value.content
? JSON.parse(workflowInfo.value.content)
: {};
});
}
async function getLlmList() {
api.get('/api/v1/model/list').then((res) => {
llmList.value = res.data;
});
}
async function getKnowledgeList() {
api.get('/api/v1/documentCollection/list').then((res) => {
knowledgeList.value = res.data;
});
}
function getRunningParams() {
api
.get(`/api/v1/workflow/getRunningParameters?id=${workflowId.value}`)
.then((res) => {
if (res.errorCode === 0) {
runParams.value = res.data;
drawerVisible.value = true;
}
});
}
function onSubmit() {
initState.value = !initState.value;
}
async function runIndependently(node: any) {
if (node.type === 'loopNode') {
ElMessage.warning($t('message.notSupported'));
return;
}
await handleSave();
singleNode.value = node;
singleRunVisible.value = true;
}
function resumeChain(data: any) {
workflowForm.value?.resume(data);
}
function handleChoose(nodeName: string, value: any) {
if (nodeName === nodeNames.workflowNode) {
handleWorkflowNodeUpdate(value[0]);
}
if (nodeName === nodeNames.pluginNode) {
handlePluginNodeUpdate(value[0]);
}
}
function handleWorkflowNodeUpdate(chooseId: any) {
pageLoading.value = true;
api
.get('/api/v1/workflowNode/getChainParams', {
params: {
currentId: workflowId.value,
workflowId: chooseId,
},
})
.then((res) => {
pageLoading.value = false;
updateWorkflowNode.value(res.data);
});
}
function handlePluginNodeUpdate(chooseId: any) {
pageLoading.value = true;
api
.get('/api/v1/pluginItem/getTinyFlowData', {
params: {
id: chooseId,
},
})
.then((res) => {
pageLoading.value = false;
updatePluginNode.value(res.data);
});
}
function onAsyncExecute(info: any) {
chainInfo.value = info;
}
</script>
<template>
<div class="head-div h-full w-full" v-loading="pageLoading">
<CommonSelectDataModal
ref="workflowSelectRef"
page-url="/api/v1/workflow/page"
@get-data="(v) => handleChoose(nodeNames.workflowNode, v)"
/>
<CommonSelectDataModal
:title="$t('menus.ai.plugin')"
width="730"
ref="pluginSelectRef"
page-url="/api/v1/plugin/page"
:has-parent="true"
single-select
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"
/>
<ElDrawer
v-model="singleRunVisible"
:title="singleNode?.data?.title"
destroy-on-close
size="600px"
>
<SingleRun :node="singleNode" :workflow-id="workflowId" />
</ElDrawer>
<ElDrawer v-model="drawerVisible" :title="$t('button.run')" size="600px">
<div class="mb-2.5 font-semibold">{{ $t('aiWorkflow.params') }}</div>
<WorkflowForm
ref="workflowForm"
:workflow-id="workflowId"
:workflow-params="runParams"
:on-submit="onSubmit"
:on-async-execute="onAsyncExecute"
:tiny-flow-data="tinyFlowData"
/>
<div class="mb-2.5 font-semibold">{{ $t('aiWorkflow.steps') }}</div>
<WorkflowSteps
:workflow-id="workflowId"
:node-json="sortNodes(tinyFlowData)"
:init-signal="initState"
:polling-data="chainInfo"
@resume="resumeChain"
/>
<div class="mb-2.5 mt-2.5 font-semibold">
{{ $t('aiWorkflow.result') }}
</div>
<ExecResult
:workflow-id="workflowId"
:node-json="sortNodes(tinyFlowData)"
:init-signal="initState"
:polling-data="chainInfo"
/>
</ElDrawer>
<div class="flex items-center justify-between border-b p-2.5">
<div>
<ElButton :icon="ArrowLeft" link @click="router.back()">
<span
class="max-w-[500px] overflow-hidden text-ellipsis text-nowrap text-base"
style="font-size: 14px"
:title="workflowInfo.title"
>
{{ workflowInfo.title }}
</span>
</ElButton>
</div>
<div>
<ElButton :disabled="saveLoading" :icon="Position" @click="runWorkflow">
{{ $t('button.runTest') }}
</ElButton>
<ElButton
type="primary"
:disabled="saveLoading"
@click="handleSave(true)"
>
{{ $t('button.save') }}(ctrl+s)
</ElButton>
</div>
</div>
<Tinyflow
ref="tinyflowRef"
v-if="showTinyFlow"
class="tiny-flow-container"
:data="JSON.parse(JSON.stringify(tinyFlowData))"
:provider="provider"
:custom-nodes="customNode"
:on-node-execute="runIndependently"
/>
<ElSkeleton class="load-div" v-else :rows="5" animated />
</div>
</template>
<style scoped>
:deep(.tf-toolbar-container-body) {
height: calc(100vh - 365px) !important;
overflow-y: auto;
}
:deep(.agentsflow) {
height: calc(100vh - 130px) !important;
}
.head-div {
background-color: var(--el-bg-color);
}
.tiny-flow-container {
width: 100%;
height: calc(100vh - 150px);
}
.load-div {
margin: 20px;
}
</style>

View File

@@ -0,0 +1,451 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import type { ActionButton } from '#/components/page/CardList.vue';
import { computed, markRaw, onMounted, ref } from 'vue';
import {
CopyDocument,
Delete,
Download,
Edit,
Plus,
Tickets,
Upload,
VideoPlay,
} from '@element-plus/icons-vue';
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
} from 'element-plus';
import { tryit } from 'radash';
import { api } from '#/api/request';
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
// import workflowSvg from '#/assets/workflow.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import DesignIcon from '#/components/icons/DesignIcon.vue';
import CardList from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import { $t } from '#/locales';
import { router } from '#/router';
import { useDictStore } from '#/store';
import WorkflowModal from './WorkflowModal.vue';
interface FieldDefinition {
// 字段名称
prop: string;
// 字段标签
label: string;
// 字段类型input, number, select, radio, checkbox, switch, date, datetime
type?: 'input' | 'number';
// 是否必填
required?: boolean;
// 占位符
placeholder?: string;
}
const actions: ActionButton[] = [
{
icon: Edit,
text: $t('button.edit'),
className: '',
permission: '/api/v1/workflow/save',
onClick: (row: any) => {
showDialog(row);
},
},
{
icon: DesignIcon,
text: $t('button.design'),
className: '',
permission: '/api/v1/workflow/save',
onClick: (row: any) => {
toDesignPage(row);
},
},
{
icon: VideoPlay,
text: $t('button.run'),
className: '',
permission: '',
onClick: (row: any) => {
router.push({
name: 'RunPage',
query: {
id: row.id,
},
});
},
},
{
icon: Tickets,
text: $t('aiWorkflowExecRecord.moduleName'),
className: '',
permission: '/api/v1/workflow/save',
onClick: (row: any) => {
router.push({
name: 'ExecRecord',
query: {
workflowId: row.id,
},
});
},
},
{
icon: Download,
text: $t('button.export'),
className: '',
permission: '',
onClick: (row: any) => {
exportJson(row);
},
},
{
icon: CopyDocument,
text: $t('button.copy'),
className: '',
permission: '',
onClick: (row: any) => {
showDialog({
title: `${row.title}Copy`,
content: row.content,
});
},
},
{
icon: Delete,
text: $t('button.delete'),
className: 'item-danger',
permission: '',
onClick: (row: any) => {
remove(row);
},
},
];
onMounted(() => {
initDict();
getSideList();
});
const pageDataRef = ref();
const saveDialog = ref();
const dictStore = useDictStore();
const headerButtons = [
{
key: 'create',
text: $t('button.add'),
icon: markRaw(Plus),
type: 'primary',
data: { action: 'create' },
permission: '/api/v1/workflow/save',
},
{
key: 'import',
text: $t('button.import'),
icon: markRaw(Upload),
type: 'default',
data: { action: 'import' },
permission: '/api/v1/workflow/save',
},
];
function initDict() {
dictStore.fetchDictionary('dataStatus');
}
const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
};
function reset() {
pageDataRef.value.setQuery({});
}
function showDialog(row: any, importMode = false) {
saveDialog.value.openDialog({ ...row }, importMode);
}
function remove(row: any) {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
api
.post('/api/v1/workflow/remove', { id: row.id })
.then((res) => {
instance.confirmButtonLoading = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
reset();
done();
}
})
.catch(() => {
instance.confirmButtonLoading = false;
});
} else {
done();
}
},
}).catch(() => {});
}
function toDesignPage(row: any) {
router.push({
name: 'WorkflowDesign',
query: {
id: row.id,
},
});
}
function exportJson(row: any) {
api
.get('/api/v1/workflow/exportWorkFlow', {
params: {
id: row.id,
},
})
.then((res) => {
const text = res.data;
const element = document.createElement('a');
element.setAttribute(
'href',
`data:text/plain;charset=utf-8,${encodeURIComponent(text)}`,
);
element.setAttribute('download', `${row.title}.json`);
element.style.display = 'none';
document.body.append(element);
element.click();
element.remove();
ElMessage.success($t('message.downloadSuccess'));
});
}
const fieldDefinitions = ref<FieldDefinition[]>([
{
prop: 'categoryName',
label: $t('aiWorkflowCategory.categoryName'),
type: 'input',
required: true,
placeholder: $t('aiWorkflowCategory.categoryName'),
},
{
prop: 'sortNo',
label: $t('aiWorkflowCategory.sortNo'),
type: 'number',
required: false,
placeholder: $t('aiWorkflowCategory.sortNo'),
},
]);
const formData = ref<any>({});
const dialogVisible = ref(false);
const formRef = ref<FormInstance>();
const saveLoading = ref(false);
const sideList = ref<any[]>([]);
const controlBtns = [
{
icon: Edit,
label: $t('button.edit'),
onClick(row: any) {
showControlDialog(row);
},
},
{
type: 'danger',
icon: Delete,
label: $t('button.delete'),
onClick(row: any) {
removeCategory(row);
},
},
];
const footerButton = {
icon: Plus,
label: $t('button.add'),
onClick() {
showControlDialog({});
},
};
const formRules = computed(() => {
const rules: Record<string, any[]> = {};
fieldDefinitions.value.forEach((field) => {
const fieldRules = [];
if (field.required) {
fieldRules.push({
required: true,
message: `${$t('message.required')}`,
trigger: 'blur',
});
}
if (fieldRules.length > 0) {
rules[field.prop] = fieldRules;
}
});
return rules;
});
function changeCategory(category: any) {
pageDataRef.value.setQuery({ categoryId: category.id });
}
function showControlDialog(item: any) {
formRef.value?.resetFields();
formData.value = { ...item };
dialogVisible.value = true;
}
function removeCategory(row: any) {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
api
.post('/api/v1/workflowCategory/remove', { id: row.id })
.then((res) => {
instance.confirmButtonLoading = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
done();
getSideList();
}
})
.catch(() => {
instance.confirmButtonLoading = false;
});
} else {
done();
}
},
}).catch(() => {});
}
function handleSubmit() {
formRef.value?.validate((valid) => {
if (valid) {
saveLoading.value = true;
const url = formData.value.id
? '/api/v1/workflowCategory/update'
: '/api/v1/workflowCategory/save';
api.post(url, formData.value).then((res) => {
saveLoading.value = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
dialogVisible.value = false;
getSideList();
}
});
}
});
}
const getSideList = async () => {
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
});
if (res && res.errorCode === 0) {
sideList.value = [
{
id: '',
categoryName: $t('common.allCategories'),
},
...res.data,
];
}
};
function handleHeaderButtonClick(data: any) {
if (data.data.action === 'import') {
showDialog({}, true);
} else {
showDialog({});
}
}
</script>
<template>
<div class="flex h-full flex-col gap-6 p-6">
<WorkflowModal ref="saveDialog" @reload="reset" />
<HeaderSearch
:buttons="headerButtons"
@search="handleSearch"
@button-click="handleHeaderButtonClick"
/>
<div class="flex max-h-[calc(100vh-191px)] flex-1 gap-6">
<PageSide
label-key="categoryName"
value-key="id"
:menus="sideList"
:control-btns="controlBtns"
:footer-button="footerButton"
@change="changeCategory"
/>
<div class="h-full flex-1 overflow-auto">
<PageData
ref="pageDataRef"
page-url="/api/v1/workflow/page"
:page-sizes="[12, 18, 24]"
:page-size="12"
>
<template #default="{ pageList }">
<CardList
:default-icon="workflowIcon"
:data="pageList"
:actions="actions"
/>
</template>
</PageData>
</div>
</div>
<ElDialog
v-model="dialogVisible"
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
:close-on-click-modal="false"
>
<ElForm
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
>
<!-- 动态生成表单项 -->
<ElFormItem
v-for="field in fieldDefinitions"
:key="field.prop"
:label="field.label"
:prop="field.prop"
>
<ElInput
v-if="!field.type || field.type === 'input'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
/>
<ElInputNumber
v-else-if="field.type === 'number'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
style="width: 100%"
/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">
{{ $t('button.cancel') }}
</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
{{ $t('button.confirm') }}
</ElButton>
</template>
</ElDialog>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,231 @@
<script setup lang="ts">
import type { FormInstance, UploadInstance, UploadProps } from 'element-plus';
import { computed, onMounted, ref } from 'vue';
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElUpload,
} from 'element-plus';
import { api } from '#/api/request';
import DictSelect from '#/components/dict/DictSelect.vue';
// import Cropper from '#/components/upload/Cropper.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
import { $t } from '#/locales';
const emit = defineEmits(['reload']);
// vue
onMounted(() => {});
defineExpose({
openDialog,
});
const saveForm = ref<FormInstance>();
// variables
const dialogVisible = ref(false);
const isAdd = ref(true);
const isImport = ref(false);
const jsonFile = ref<any>(null);
const uploadFileList = ref<any[]>([]);
const uploadRef = ref<UploadInstance>();
const entity = ref<any>({
alias: '',
deptId: '',
title: '',
description: '',
icon: '',
content: '',
englishName: '',
});
const btnLoading = ref(false);
const jsonFileModel = computed({
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
set: (value: any) => {
if (!value) {
uploadFileList.value = [];
}
},
});
const rules = computed(() => ({
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
...(isImport.value && {
jsonFile: [
{ required: true, message: $t('message.required'), trigger: 'change' },
],
}),
}));
// functions
function openDialog(row: any, importMode = false) {
isImport.value = importMode;
if (row.id) {
isAdd.value = false;
}
entity.value = row;
dialogVisible.value = true;
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
jsonFile.value = file;
uploadFileList.value = [file];
saveForm.value?.clearValidate('jsonFile');
return false;
};
const handleChange: UploadProps['onChange'] = (file, fileList) => {
jsonFile.value = file.raw;
uploadFileList.value = fileList.slice(-1);
saveForm.value?.clearValidate('jsonFile');
};
const handleRemove: UploadProps['onRemove'] = () => {
jsonFile.value = null;
uploadFileList.value = [];
saveForm.value?.clearValidate('jsonFile');
};
function save() {
saveForm.value?.validate((valid) => {
if (valid) {
btnLoading.value = true;
if (isImport.value) {
const formData = new FormData();
formData.append('jsonFile', jsonFile.value!);
Object.keys(entity.value).forEach((key) => {
if (entity.value[key] !== null && entity.value[key] !== undefined) {
formData.append(key, entity.value[key]);
}
});
api
.post('/api/v1/workflow/importWorkFlow', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((res) => {
btnLoading.value = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
emit('reload');
closeDialog();
}
})
.catch(() => {
btnLoading.value = false;
});
} else {
api
.post(
isAdd.value ? '/api/v1/workflow/save' : '/api/v1/workflow/update',
entity.value,
)
.then((res) => {
btnLoading.value = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
emit('reload');
closeDialog();
}
})
.catch(() => {
btnLoading.value = false;
});
}
}
});
}
function closeDialog() {
saveForm.value?.resetFields();
uploadRef.value?.clearFiles();
uploadFileList.value = [];
jsonFile.value = null;
isAdd.value = true;
isImport.value = false;
entity.value = {};
dialogVisible.value = false;
}
</script>
<template>
<ElDialog
v-model="dialogVisible"
draggable
:title="
isImport
? $t('button.import')
: isAdd
? $t('button.add')
: $t('button.edit')
"
:before-close="closeDialog"
:close-on-click-modal="false"
>
<ElForm
label-width="120px"
ref="saveForm"
:model="isImport ? { ...entity, jsonFile: jsonFileModel } : entity"
status-icon
:rules="rules"
>
<ElFormItem v-if="isImport" prop="jsonFile" label="JSON文件" required>
<ElUpload
class="w-full"
ref="uploadRef"
v-model:file-list="uploadFileList"
:limit="1"
:auto-upload="false"
:on-change="handleChange"
:before-upload="beforeUpload"
:on-remove="handleRemove"
accept=".json"
drag
>
<div class="el-upload__text w-full">
json 文件拖到此处<em>点击上传</em>
</div>
</ElUpload>
</ElFormItem>
<ElFormItem prop="icon" :label="$t('aiWorkflow.icon')">
<!-- <Cropper v-model="entity.icon" crop /> -->
<UploadAvatar v-model="entity.icon" />
</ElFormItem>
<ElFormItem prop="title" :label="$t('aiWorkflow.title')">
<ElInput v-model.trim="entity.title" />
</ElFormItem>
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
<DictSelect
v-model="entity.categoryId"
dict-code="aiWorkFlowCategory"
/>
</ElFormItem>
<ElFormItem prop="alias" :label="$t('aiWorkflow.alias')">
<ElInput v-model.trim="entity.alias" />
</ElFormItem>
<ElFormItem prop="englishName" :label="$t('aiWorkflow.englishName')">
<ElInput v-model.trim="entity.englishName" />
</ElFormItem>
<ElFormItem prop="description" :label="$t('aiWorkflow.description')">
<ElInput v-model.trim="entity.description" />
</ElFormItem>
<ElFormItem prop="status" :label="$t('aiWorkflow.status')">
<DictSelect v-model="entity.status" dict-code="showOrNot" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="closeDialog">
{{ $t('button.cancel') }}
</ElButton>
<ElButton
type="primary"
@click="save"
:loading="btnLoading"
:disabled="btnLoading"
>
{{ $t('button.save') }}
</ElButton>
</template>
</ElDialog>
</template>
<style scoped></style>

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>

View File

@@ -0,0 +1,37 @@
import { $t } from '#/locales';
import nodeNames from './nodeNames';
export default {
[nodeNames.documentNode]: {
title: $t('aiWorkflow.fileContentExtraction'),
group: 'base',
description: $t('aiWorkflow.descriptions.fileContentExtraction'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21 4V18.7215C21 18.9193 20.8833 19.0986 20.7024 19.1787L12 23.0313L3.29759 19.1787C3.11667 19.0986 3 18.9193 3 18.7215V4H1V2H23V4H21ZM5 4V17.7451L12 20.8441L19 17.7451V4H5ZM8 8H16V10H8V8ZM8 12H16V14H8V12Z"></path></svg>',
sortNo: 801,
parametersAddEnable: false,
outputDefsAddEnable: false,
parameters: [
{
name: 'fileUrl',
nameDisabled: true,
title: $t('aiWorkflow.documentAddress'),
dataType: 'File',
required: true,
description: $t('aiWorkflow.descriptions.documentAddress'),
},
],
outputDefs: [
{
name: 'content',
title: $t('aiWorkflow.parsedText'),
dataType: 'String',
dataTypeDisabled: true,
required: true,
parametersAddEnable: false,
description: $t('aiWorkflow.descriptions.parsedText'),
deleteDisabled: true,
},
],
},
};

View File

@@ -0,0 +1,90 @@
import { $t } from '#/locales';
import nodeNames from './nodeNames';
export default {
[nodeNames.downloadNode]: {
title: $t('aiWorkflow.resourceSync'),
group: 'base',
description: $t('aiWorkflow.descriptions.resourceSync'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 12H16L12 16L8 12H11V8H13V12ZM15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z"></path></svg>',
sortNo: 811,
parametersAddEnable: false,
outputDefsAddEnable: false,
parameters: [
{
name: 'originUrl',
nameDisabled: true,
title: $t('aiWorkflow.originUrl'),
dataType: 'String',
required: true,
description: $t('aiWorkflow.descriptions.originUrl'),
},
],
outputDefs: [
{
name: 'resourceUrl',
title: $t('aiWorkflow.savedUrl'),
dataType: 'String',
dataTypeDisabled: true,
required: true,
parametersAddEnable: false,
description: $t('aiWorkflow.savedUrl'),
deleteDisabled: true,
},
],
forms: [
// 节点表单
{
// 'input' | 'textarea' | 'select' | 'slider' | 'heading' | 'chosen'
type: 'heading',
label: $t('aiWorkflow.saveOptions'),
},
{
type: 'select',
label: $t('aiResource.resourceType'),
description: $t('aiWorkflow.descriptions.resourceType'),
name: 'resourceType', // 属性名称
defaultValue: '99',
options: [
{
label: $t('aiWorkflow.image'),
value: '0',
},
{
label: $t('aiWorkflow.video'),
value: '1',
},
{
label: $t('aiWorkflow.audio'),
value: '2',
},
{
label: $t('aiWorkflow.document'),
value: '3',
},
{
label: $t('aiWorkflow.other'),
value: '99',
},
],
},
// {
// // 用法可参考插件节点的代码
// type: 'chosen',
// label: '插件选择',
// chosen: {
// // 节点自定义属性
// labelDataKey: 'pluginName',
// valueDataKey: 'pluginId',
// // updateNodeData 可动态更新节点属性
// // value 为选中的 value
// // label 为选中的 label
// onChosen: ((updateNodeData: (data: Record<string, any>) => void, value?: string, label?: string, event?: Event) => {
// console.warn('No onChosen handler provided for plugin-node');
// })
// }
// }
],
},
};

View File

@@ -0,0 +1,29 @@
import docNode from './documentNode';
import downloadNode from './downloadNode';
import makeFileNode from './makeFileNode';
import nodeNames from './nodeNames';
import { PluginNode } from './pluginNode';
import { SaveToDatacenterNode } from './saveToDatacenter';
import { SearchDatacenterNode } from './searchDatacenter';
import sqlNode from './sqlNode';
import { WorkflowNode } from './workflowNode';
export interface CustomNodeOptions {
handleChosen?: (nodeType: string, updateNodeData: any, value: string) => void;
}
export const getCustomNode = async (options: CustomNodeOptions) => {
const pluginNode = PluginNode({ onChosen: options.handleChosen });
const workflowNode = WorkflowNode({ onChosen: options.handleChosen });
const searchDatacenterNode = await SearchDatacenterNode();
const saveToDatacenterNode = await SaveToDatacenterNode();
return {
...docNode,
...makeFileNode,
...downloadNode,
...sqlNode,
[nodeNames.pluginNode]: pluginNode,
[nodeNames.workflowNode]: workflowNode,
[nodeNames.searchDatacenterNode]: searchDatacenterNode,
[nodeNames.saveToDatacenterNode]: saveToDatacenterNode,
};
};

View File

@@ -0,0 +1,58 @@
import { $t } from '#/locales';
import nodeNames from './nodeNames';
export default {
[nodeNames.makeFileNode]: {
title: $t('aiWorkflow.fileGeneration'),
group: 'base',
description: $t('aiWorkflow.descriptions.fileGeneration'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M14 13.5V8C14 5.79086 12.2091 4 10 4C7.79086 4 6 5.79086 6 8V13.5C6 17.0899 8.91015 20 12.5 20C16.0899 20 19 17.0899 19 13.5V4H21V13.5C21 18.1944 17.1944 22 12.5 22C7.80558 22 4 18.1944 4 13.5V8C4 4.68629 6.68629 2 10 2C13.3137 2 16 4.68629 16 8V13.5C16 15.433 14.433 17 12.5 17C10.567 17 9 15.433 9 13.5V8H11V13.5C11 14.3284 11.6716 15 12.5 15C13.3284 15 14 14.3284 14 13.5Z"></path></svg>',
sortNo: 802,
parametersAddEnable: true,
outputDefsAddEnable: true,
forms: [
{
type: 'heading',
label: $t('aiWorkflow.fileSettings'),
},
{
type: 'select',
label: $t('documentCollection.splitterDoc.fileType'),
description: $t('aiWorkflow.descriptions.fileType'),
name: 'suffix',
defaultValue: 'docx',
options: [
{
label: 'docx',
value: 'docx',
},
],
},
],
parameters: [
{
name: 'content',
nameDisabled: true,
title: $t('preferences.content'),
dataType: 'String',
required: true,
description: $t('preferences.content'),
deleteDisabled: true,
},
],
outputDefs: [
{
name: 'url',
nameDisabled: true,
title: $t('aiWorkflow.fileDownloadURL'),
dataType: 'String',
dataTypeDisabled: true,
required: true,
parametersAddEnable: false,
description: $t('aiWorkflow.descriptions.fileDownloadURL'),
deleteDisabled: true,
},
],
},
};

View File

@@ -0,0 +1,10 @@
export default {
documentNode: 'document-node',
makeFileNode: 'make-file',
downloadNode: 'download-node',
sqlNode: 'sql-node',
pluginNode: 'plugin-node',
workflowNode: 'workflow-node',
searchDatacenterNode: 'search-datacenter-node',
saveToDatacenterNode: 'save-to-datacenter-node',
};

View File

@@ -0,0 +1,30 @@
import { $t } from '#/locales';
import nodeNames from './nodeNames';
export interface PluginNodeOptions {
onChosen?: (nodeType: string, updateNodeData: any, value: string) => void;
}
export const PluginNode = (options: PluginNodeOptions = {}) => ({
title: $t('menus.ai.plugin'),
group: 'base',
description: $t('aiWorkflow.descriptions.plugin'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 18V20H19V22H13C11.8954 22 11 21.1046 11 20V18H8C5.79086 18 4 16.2091 4 14V7C4 6.44772 4.44772 6 5 6H8V2H10V6H14V2H16V6H19C19.5523 6 20 6.44772 20 7V14C20 16.2091 18.2091 18 16 18H13ZM8 16H16C17.1046 16 18 15.1046 18 14V11H6V14C6 15.1046 6.89543 16 8 16ZM18 8H6V9H18V8ZM12 14.5C11.4477 14.5 11 14.0523 11 13.5C11 12.9477 11.4477 12.5 12 12.5C12.5523 12.5 13 12.9477 13 13.5C13 14.0523 12.5523 14.5 12 14.5Z"></path></svg>',
sortNo: 810,
parametersAddEnable: false,
outputDefsAddEnable: false,
forms: [
{
type: 'chosen',
label: $t('aiWorkflow.pluginSelect'),
chosen: {
labelDataKey: 'pluginName',
valueDataKey: 'pluginId',
onChosen: (updateNodeData: any, value: any) => {
options.onChosen?.(nodeNames.pluginNode, updateNodeData, value);
},
},
},
],
});

View File

@@ -0,0 +1,58 @@
import { getOptions } from '@easyflow/utils';
import { api } from '#/api/request';
import { $t } from '#/locales';
export const SaveToDatacenterNode = async () => {
const res = await api.get('/api/v1/datacenterTable/list');
return {
title: $t('aiWorkflow.saveData'),
group: 'base',
description: $t('aiWorkflow.descriptions.saveData'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 19V9H4V19H11ZM11 7V4C11 3.44772 11.4477 3 12 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V8C2 7.44772 2.44772 7 3 7H11ZM13 5V19H20V5H13ZM5 16H10V18H5V16ZM14 16H19V18H14V16ZM14 13H19V15H14V13ZM14 10H19V12H14V10ZM5 13H10V15H5V13Z"></path></svg>',
sortNo: 812,
parametersAddEnable: false,
outputDefsAddEnable: false,
parameters: [
{
name: 'saveList',
title: $t('aiWorkflow.dataToBeSaved'),
dataType: 'Array',
dataTypeDisabled: true,
required: true,
parametersAddEnable: false,
description: $t('aiWorkflow.descriptions.dataToBeSaved'),
deleteDisabled: true,
nameDisabled: true,
},
],
outputDefs: [
{
name: 'successRows',
title: $t('aiWorkflow.successInsertedRecords'),
dataType: 'Number',
dataTypeDisabled: true,
required: true,
parametersAddEnable: false,
description: $t('aiWorkflow.successInsertedRecords'),
deleteDisabled: true,
nameDisabled: true,
},
],
forms: [
{
type: 'heading',
label: $t('aiWorkflow.dataTable'),
},
{
type: 'select',
label: '',
description: $t('aiWorkflow.descriptions.dataTable'),
name: 'tableId',
defaultValue: '',
options: getOptions('tableName', 'id', res.data),
},
],
};
};

View File

@@ -0,0 +1,68 @@
import { getOptions } from '@easyflow/utils';
import { api } from '#/api/request';
import { $t } from '#/locales';
export const SearchDatacenterNode = async () => {
const res = await api.get('/api/v1/datacenterTable/list');
return {
title: $t('aiWorkflow.queryData'),
group: 'base',
description: $t('aiWorkflow.descriptions.queryData'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 2C15.968 2 20 6.032 20 11C20 15.968 15.968 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2ZM11 18C14.8675 18 18 14.8675 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18ZM19.4853 18.0711L22.3137 20.8995L20.8995 22.3137L18.0711 19.4853L19.4853 18.0711Z"></path></svg>',
sortNo: 813,
parametersAddEnable: true,
outputDefsAddEnable: false,
parameters: [],
outputDefs: [
{
name: 'rows',
title: $t('aiWorkflow.queryResult'),
dataType: 'Array',
dataTypeDisabled: true,
required: true,
parametersAddEnable: false,
description: $t('aiWorkflow.queryResult'),
deleteDisabled: true,
nameDisabled: false,
},
],
forms: [
{
type: 'heading',
label: $t('aiWorkflow.dataTable'),
},
{
type: 'select',
label: '',
description: $t('aiWorkflow.descriptions.dataTable'),
name: 'tableId',
defaultValue: '',
options: getOptions('tableName', 'id', res.data),
},
{
type: 'heading',
label: $t('aiWorkflow.filterConditions'),
},
{
type: 'textarea',
label: "如name='张三' and age=21 or field = {{流程变量}}",
description: '',
name: 'where',
defaultValue: '',
},
{
type: 'heading',
label: $t('aiWorkflow.limit'),
},
{
type: 'input',
label: '',
description: '',
name: 'limit',
defaultValue: '10',
},
],
};
};

View File

@@ -0,0 +1,37 @@
import { $t } from '#/locales';
import nodeNames from './nodeNames';
export default {
[nodeNames.sqlNode]: {
title: $t('aiWorkflow.sqlQuery'),
group: 'base',
description: $t('aiWorkflow.descriptions.sqlQuery'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgba(37,99,235,1)"><path d="M5 12.5C5 12.8134 5.46101 13.3584 6.53047 13.8931C7.91405 14.5849 9.87677 15 12 15C14.1232 15 16.0859 14.5849 17.4695 13.8931C18.539 13.3584 19 12.8134 19 12.5V10.3287C17.35 11.3482 14.8273 12 12 12C9.17273 12 6.64996 11.3482 5 10.3287V12.5ZM19 15.3287C17.35 16.3482 14.8273 17 12 17C9.17273 17 6.64996 16.3482 5 15.3287V17.5C5 17.8134 5.46101 18.3584 6.53047 18.8931C7.91405 19.5849 9.87677 20 12 20C14.1232 20 16.0859 19.5849 17.4695 18.8931C18.539 18.3584 19 17.8134 19 17.5V15.3287ZM3 17.5V7.5C3 5.01472 7.02944 3 12 3C16.9706 3 21 5.01472 21 7.5V17.5C21 19.9853 16.9706 22 12 22C7.02944 22 3 19.9853 3 17.5ZM12 10C14.1232 10 16.0859 9.58492 17.4695 8.89313C18.539 8.3584 19 7.81342 19 7.5C19 7.18658 18.539 6.6416 17.4695 6.10687C16.0859 5.41508 14.1232 5 12 5C9.87677 5 7.91405 5.41508 6.53047 6.10687C5.46101 6.6416 5 7.18658 5 7.5C5 7.81342 5.46101 8.3584 6.53047 8.89313C7.91405 9.58492 9.87677 10 12 10Z"></path></svg>',
sortNo: 803,
parametersAddEnable: true,
outputDefsAddEnable: true,
parameters: [],
forms: [
{
name: 'sql',
type: 'textarea',
label: 'SQL',
placeholder: $t('aiWorkflow.descriptions.enterSQL'),
},
],
outputDefs: [
{
name: 'queryData',
title: $t('aiWorkflow.queryResult'),
dataType: 'Array',
dataTypeDisabled: true,
required: true,
parametersAddEnable: false,
description: $t('aiWorkflow.descriptions.queryResultJson'),
deleteDisabled: true,
nameDisabled: true,
},
],
},
};

View File

@@ -0,0 +1,30 @@
import { $t } from '#/locales';
import nodeNames from './nodeNames';
export interface WorkflowNodeOptions {
onChosen?: (nodeType: string, updateNodeData: any, value: string) => void;
}
export const WorkflowNode = (options: WorkflowNodeOptions = {}) => ({
title: $t('aiWorkflow.subProcess'),
group: 'base',
description: $t('aiWorkflow.descriptions.subProcess'),
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 21.5C4.067 21.5 2.5 19.933 2.5 18C2.5 16.067 4.067 14.5 6 14.5C7.5852 14.5 8.92427 15.5539 9.35481 16.9992L15 16.9994V15L17 14.9994V9.24339L14.757 6.99938H9V9.00003H3V3.00003H9V4.99939H14.757L18 1.75739L22.2426 6.00003L19 9.24139V14.9994L21 15V21H15V18.9994L9.35499 19.0003C8.92464 20.4459 7.58543 21.5 6 21.5ZM6 16.5C5.17157 16.5 4.5 17.1716 4.5 18C4.5 18.8285 5.17157 19.5 6 19.5C6.82843 19.5 7.5 18.8285 7.5 18C7.5 17.1716 6.82843 16.5 6 16.5ZM19 17H17V19H19V17ZM18 4.58581L16.5858 6.00003L18 7.41424L19.4142 6.00003L18 4.58581ZM7 5.00003H5V7.00003H7V5.00003Z"></path></svg>',
sortNo: 815,
parametersAddEnable: false,
outputDefsAddEnable: false,
forms: [
{
type: 'chosen',
label: $t('aiWorkflow.workflowSelect'),
chosen: {
labelDataKey: 'workflowName',
valueDataKey: 'workflowId',
onChosen: (updateNodeData: any, value: any) => {
options.onChosen?.(nodeNames.workflowNode, updateNodeData, value);
},
},
},
],
});

View File

@@ -0,0 +1,244 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ArrowLeft, DeleteFilled, MoreFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElMessageBox,
ElTable,
ElTableColumn,
ElTag,
} from 'element-plus';
import { api } from '#/api/request';
import PageData from '#/components/page/PageData.vue';
import { $t } from '#/locales';
import { useDictStore } from '#/store';
const router = useRouter();
const $route = useRoute();
onMounted(() => {
initDict();
});
const formRef = ref<FormInstance>();
const pageDataRef = ref();
const formInline = ref({
execKey: '',
});
const dictStore = useDictStore();
function initDict() {
dictStore.fetchDictionary('dataStatus');
}
function search(formEl: FormInstance | undefined) {
formEl?.validate((valid) => {
if (valid) {
pageDataRef.value.setQuery(formInline.value);
}
});
}
function reset(formEl: FormInstance | undefined) {
formEl?.resetFields();
pageDataRef.value.setQuery({});
}
function remove(row: any) {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
api
.get('/api/v1/workflowExecResult/del', { params: { id: row.id } })
.then((res) => {
instance.confirmButtonLoading = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
reset(formRef.value);
done();
}
})
.catch(() => {
instance.confirmButtonLoading = false;
});
} else {
done();
}
},
}).catch(() => {});
}
function toStepPage(row: any) {
router.push({
name: 'RecordStep',
query: {
recordId: row.id,
},
});
}
function getTagType(row: any) {
switch (row.status) {
case 1: {
return 'primary';
}
case 5: {
return 'warning';
}
case 10: {
return 'danger';
}
case 20: {
return 'success';
}
case 21: {
return 'danger';
}
default: {
return 'info';
}
}
}
</script>
<template>
<div class="page-container border-border border">
<div class="mb-3">
<ElButton :icon="ArrowLeft" @click="router.back()">
{{ $t('button.back') }}
</ElButton>
</div>
<ElForm ref="formRef" :inline="true" :model="formInline">
<ElFormItem class="w-full max-w-[300px]" prop="execKey">
<ElInput
v-model="formInline.execKey"
:placeholder="$t('aiWorkflowExecRecord.execKey')"
/>
</ElFormItem>
<ElFormItem>
<ElButton @click="search(formRef)" type="primary">
{{ $t('button.query') }}
</ElButton>
<ElButton @click="reset(formRef)">
{{ $t('button.reset') }}
</ElButton>
</ElFormItem>
</ElForm>
<div class="handle-div"></div>
<PageData
ref="pageDataRef"
page-url="/api/v1/workflowExecResult/page"
:page-size="10"
:extra-query-params="{
workflowId: $route.query.workflowId,
}"
>
<template #default="{ pageList }">
<ElTable :data="pageList" border>
<ElTableColumn
prop="execKey"
show-overflow-tooltip
:label="$t('aiWorkflowExecRecord.execKey')"
>
<template #default="{ row }">
{{ row.execKey }}
</template>
</ElTableColumn>
<ElTableColumn prop="title" :label="$t('aiWorkflowExecRecord.title')">
<template #default="{ row }">
{{ row.title }}
</template>
</ElTableColumn>
<ElTableColumn
prop="description"
:label="$t('aiWorkflowExecRecord.description')"
>
<template #default="{ row }">
{{ row.description }}
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
prop="input"
:label="$t('aiWorkflowExecRecord.input')"
>
<template #default="{ row }">
{{ row.input }}
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
prop="output"
:label="$t('aiWorkflowExecRecord.output')"
>
<template #default="{ row }">
{{ row.output }}
</template>
</ElTableColumn>
<ElTableColumn
prop="startTime"
:label="$t('aiWorkflowExecRecord.execTime')"
>
<template #default="{ row }">
{{ row.execTime || '-' }} ms
</template>
</ElTableColumn>
<ElTableColumn
prop="status"
:label="$t('aiWorkflowExecRecord.status')"
>
<template #default="{ row }">
<ElTag :type="getTagType(row)">
{{ $t(`aiWorkflowExecRecord.status${row.status}`) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
prop="errorInfo"
:label="$t('aiWorkflowExecRecord.errorInfo')"
>
<template #default="{ row }">
{{ row.errorInfo }}
</template>
</ElTableColumn>
<ElTableColumn :label="$t('common.handle')" width="110" align="right">
<template #default="{ row }">
<div class="flex items-center gap-1">
<ElButton link type="primary" @click="toStepPage(row)">
{{ $t('aiWorkflowRecordStep.moduleName') }}
</ElButton>
<ElDropdown>
<ElButton :icon="MoreFilled" link />
<template #dropdown>
<ElDropdownMenu>
<div v-access:code="'/api/v1/workflow/save'">
<ElDropdownItem @click="remove(row)">
<ElButton type="danger" :icon="DeleteFilled" link>
{{ $t('button.delete') }}
</ElButton>
</ElDropdownItem>
</div>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
</ElTableColumn>
</ElTable>
</template>
</PageData>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,184 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ArrowLeft } from '@element-plus/icons-vue';
import {
ElButton,
ElForm,
ElFormItem,
ElInput,
ElTable,
ElTableColumn,
ElTag,
} from 'element-plus';
import PageData from '#/components/page/PageData.vue';
import { $t } from '#/locales';
import { useDictStore } from '#/store';
const router = useRouter();
const $route = useRoute();
onMounted(() => {
initDict();
});
const formRef = ref<FormInstance>();
const pageDataRef = ref();
const formInline = ref({
nodeName: '',
});
const dictStore = useDictStore();
function initDict() {
dictStore.fetchDictionary('dataStatus');
}
function search(formEl: FormInstance | undefined) {
formEl?.validate((valid) => {
if (valid) {
pageDataRef.value.setQuery(formInline.value);
}
});
}
function reset(formEl: FormInstance | undefined) {
formEl?.resetFields();
pageDataRef.value.setQuery({});
}
function getTagType(row: any) {
switch (row.status) {
case 1: {
return 'primary';
}
case 6: {
return 'warning';
}
case 10: {
return 'danger';
}
case 20: {
return 'success';
}
case 21: {
return 'danger';
}
default: {
return 'info';
}
}
}
</script>
<template>
<div class="page-container border-border border">
<div class="mb-3">
<ElButton :icon="ArrowLeft" @click="router.back()">
{{ $t('button.back') }}
</ElButton>
</div>
<ElForm ref="formRef" :inline="true" :model="formInline">
<ElFormItem class="w-full max-w-[300px]" prop="nodeName">
<ElInput
v-model="formInline.nodeName"
:placeholder="$t('aiWorkflowRecordStep.nodeName')"
/>
</ElFormItem>
<ElFormItem>
<ElButton @click="search(formRef)" type="primary">
{{ $t('button.query') }}
</ElButton>
<ElButton @click="reset(formRef)">
{{ $t('button.reset') }}
</ElButton>
</ElFormItem>
</ElForm>
<div class="handle-div"></div>
<PageData
ref="pageDataRef"
page-url="/api/v1/workflowExecStep/page"
:page-size="10"
:extra-query-params="{
recordId: $route.query.recordId,
sortKey: 'startTime',
sortType: 'asc',
}"
>
<template #default="{ pageList }">
<ElTable :data="pageList" border>
<ElTableColumn
show-overflow-tooltip
prop="execKey"
:label="$t('aiWorkflowRecordStep.execKey')"
>
<template #default="{ row }">
{{ row.execKey }}
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
prop="nodeId"
:label="$t('aiWorkflowRecordStep.nodeId')"
>
<template #default="{ row }">
{{ row.nodeId }}
</template>
</ElTableColumn>
<ElTableColumn
prop="nodeName"
:label="$t('aiWorkflowRecordStep.nodeName')"
>
<template #default="{ row }">
{{ row.nodeName }}
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
prop="input"
:label="$t('aiWorkflowRecordStep.input')"
>
<template #default="{ row }">
{{ row.input }}
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
prop="output"
:label="$t('aiWorkflowRecordStep.output')"
>
<template #default="{ row }">
{{ row.output }}
</template>
</ElTableColumn>
<ElTableColumn
prop="execTime"
:label="$t('aiWorkflowRecordStep.execTime')"
>
<template #default="{ row }">
{{ row.execTime || '-' }} ms
</template>
</ElTableColumn>
<ElTableColumn
prop="status"
:label="$t('aiWorkflowRecordStep.status')"
>
<template #default="{ row }">
<ElTag :type="getTagType(row)">
{{ $t(`aiWorkflowRecordStep.status${row.status}`) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
prop="errorInfo"
:label="$t('aiWorkflowRecordStep.errorInfo')"
>
<template #default="{ row }">
{{ row.errorInfo }}
</template>
</ElTableColumn>
</ElTable>
</template>
</PageData>
</div>
</template>
<style scoped></style>