252 lines
6.9 KiB
Vue
252 lines
6.9 KiB
Vue
<script setup lang="ts">
|
||
import type { FormInstance } from 'element-plus';
|
||
|
||
import { computed, ref } from 'vue';
|
||
|
||
import { Position } from '@element-plus/icons-vue';
|
||
import { ElAlert, ElButton, ElForm, ElFormItem, ElMessage } from 'element-plus';
|
||
|
||
import { api } from '#/api/request';
|
||
import ShowJson from '#/components/json/ShowJson.vue';
|
||
import { $t } from '#/locales';
|
||
import WorkflowFormItem from '#/views/ai/workflow/components/WorkflowFormItem.vue';
|
||
import { buildSingleRunModel } from '../../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields';
|
||
|
||
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>('');
|
||
const singleRunModel = computed(() => buildSingleRunModel(props.node));
|
||
const isFieldMode = computed(() => singleRunModel.value.mode === 'fields');
|
||
const singleRunParameters = computed(() => singleRunModel.value.parameters);
|
||
const singleRunFields = computed(() => singleRunModel.value.fields);
|
||
const parameterDisplayNameMap = computed(() => {
|
||
return new Map(
|
||
singleRunParameters.value.map((parameter: any) => [
|
||
String(parameter.name || ''),
|
||
String(parameter.displayName || parameter.formLabel || parameter.name || ''),
|
||
]),
|
||
);
|
||
});
|
||
|
||
function buildFieldSegments(value: string) {
|
||
const source = String(value || '');
|
||
const segments: Array<{ text: string; token: boolean }> = [];
|
||
const regex = /\{\{\s*([^{}]+?)\s*}}/g;
|
||
let lastIndex = 0;
|
||
|
||
for (const match of source.matchAll(regex)) {
|
||
const matchedText = match[0] || '';
|
||
const tokenKey = String(match[1] || '').trim();
|
||
const start = match.index ?? 0;
|
||
if (start > lastIndex) {
|
||
segments.push({
|
||
text: source.slice(lastIndex, start),
|
||
token: false,
|
||
});
|
||
}
|
||
segments.push({
|
||
text: parameterDisplayNameMap.value.get(tokenKey) || tokenKey,
|
||
token: true,
|
||
});
|
||
lastIndex = start + matchedText.length;
|
||
}
|
||
|
||
if (lastIndex < source.length) {
|
||
segments.push({
|
||
text: source.slice(lastIndex),
|
||
token: false,
|
||
});
|
||
}
|
||
|
||
return segments.length > 0 ? segments : [{ text: '', token: false }];
|
||
}
|
||
|
||
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">
|
||
<template v-if="isFieldMode">
|
||
<div class="single-run-section">
|
||
<div class="single-run-section__title">字段值</div>
|
||
<ElAlert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="当前节点字段按正式配置展示;下方只需填写这些字段引用到的上游值。"
|
||
/>
|
||
<div class="single-run-field-list">
|
||
<div
|
||
v-for="field in singleRunFields"
|
||
:key="field.key"
|
||
class="single-run-field-card"
|
||
>
|
||
<div class="single-run-field-card__label">
|
||
{{ field.label }}
|
||
</div>
|
||
<div
|
||
class="single-run-field-card__value"
|
||
:class="{ 'is-multiline': field.multiline }"
|
||
>
|
||
<template v-if="field.value">
|
||
<template
|
||
v-for="(segment, index) in buildFieldSegments(field.value)"
|
||
:key="`${field.key}-${index}`"
|
||
>
|
||
<span
|
||
v-if="segment.token"
|
||
class="single-run-token-chip"
|
||
>
|
||
{{ segment.text }}
|
||
</span>
|
||
<span
|
||
v-else
|
||
class="single-run-field-card__text"
|
||
>
|
||
{{ segment.text }}
|
||
</span>
|
||
</template>
|
||
</template>
|
||
<span v-else class="single-run-field-card__placeholder">
|
||
{{ field.placeholder || '未设置' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="single-run-section">
|
||
<div class="single-run-section__title">上游引用</div>
|
||
<ElAlert
|
||
v-if="singleRunParameters.length === 0"
|
||
type="success"
|
||
:closable="false"
|
||
show-icon
|
||
title="当前字段没有引用上游参数,可直接运行。"
|
||
/>
|
||
<WorkflowFormItem
|
||
v-else
|
||
v-model:run-params="runParams"
|
||
:parameters="singleRunParameters"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<WorkflowFormItem
|
||
v-else
|
||
v-model:run-params="runParams"
|
||
:parameters="singleRunParameters"
|
||
/>
|
||
<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>
|
||
.single-run-section + .single-run-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.single-run-section__title {
|
||
margin-bottom: 10px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.single-run-field-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.single-run-field-card {
|
||
border: 1px solid var(--el-border-color-light);
|
||
border-radius: 10px;
|
||
padding: 12px;
|
||
background: var(--el-fill-color-blank);
|
||
}
|
||
|
||
.single-run-field-card__label {
|
||
margin-bottom: 8px;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.single-run-field-card__value {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-height: 20px;
|
||
line-height: 1.7;
|
||
color: var(--el-text-color-primary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.single-run-field-card__value.is-multiline {
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.single-run-field-card__text {
|
||
white-space: inherit;
|
||
}
|
||
|
||
.single-run-field-card__placeholder {
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.single-run-token-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
max-width: 100%;
|
||
padding: 1px 8px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--el-color-primary-light-5);
|
||
background: var(--el-color-primary-light-9);
|
||
color: var(--el-color-primary);
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
}
|
||
</style>
|