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