- 新增工作流插件类型、发布快照同步、实时可用性与下线影响检查 - 收口绑定候选、分类权限、间接环路校验与运行态优雅降级 - 补齐管理端工作流插件配置、详情与试运行界面及定向测试
561 lines
14 KiB
Vue
561 lines
14 KiB
Vue
<script setup lang="ts">
|
||
import type { FormInstance, FormRules } from 'element-plus';
|
||
|
||
import { computed, onMounted, ref, watch } from 'vue';
|
||
|
||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||
|
||
import { Plus, Remove } from '@element-plus/icons-vue';
|
||
import {
|
||
ElAlert,
|
||
ElButton,
|
||
ElForm,
|
||
ElFormItem,
|
||
ElIcon,
|
||
ElInput,
|
||
ElMessage,
|
||
ElOption,
|
||
ElRadio,
|
||
ElRadioGroup,
|
||
ElSelect,
|
||
} from 'element-plus';
|
||
|
||
import { api } from '#/api/request';
|
||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||
import { $t } from '#/locales';
|
||
|
||
interface HeaderItem {
|
||
label: string;
|
||
value: string;
|
||
}
|
||
|
||
interface WorkflowCandidate {
|
||
id: string;
|
||
title: string;
|
||
}
|
||
|
||
const emit = defineEmits(['reload']);
|
||
|
||
const saveForm = ref<FormInstance>();
|
||
const dialogVisible = ref(false);
|
||
const isAdd = ref(true);
|
||
const btnLoading = ref(false);
|
||
const categoryList = ref<any[]>([]);
|
||
const workflowCandidates = ref<WorkflowCandidate[]>([]);
|
||
const tempAddHeaders = ref<HeaderItem[]>([]);
|
||
const entity = ref<any>(createDefaultEntity());
|
||
|
||
const authTypeList = [
|
||
{
|
||
label: 'None',
|
||
value: 'none',
|
||
},
|
||
{
|
||
label: 'Service token / ApiKey',
|
||
value: 'apiKey',
|
||
},
|
||
];
|
||
|
||
const pluginTypeOptions = [
|
||
{
|
||
label: $t('plugin.typeHttp'),
|
||
value: 1,
|
||
},
|
||
{
|
||
label: $t('plugin.typeWorkflow'),
|
||
value: 2,
|
||
},
|
||
];
|
||
|
||
const isWorkflowType = computed(
|
||
() => Number(entity.value.type || 1) === 2,
|
||
);
|
||
|
||
const rules = computed<FormRules>(() => ({
|
||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||
description: [
|
||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||
],
|
||
baseUrl: [
|
||
{
|
||
validator: (_rule, value, callback) => {
|
||
if (!isWorkflowType.value && !value) {
|
||
callback(new Error($t('message.required')));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
workflowId: [
|
||
{
|
||
validator: (_rule, value, callback) => {
|
||
if (isWorkflowType.value && !value) {
|
||
callback(new Error($t('message.required')));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
authType: [
|
||
{
|
||
validator: (_rule, value, callback) => {
|
||
if (!isWorkflowType.value && !value) {
|
||
callback(new Error($t('message.required')));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
position: [
|
||
{
|
||
validator: (_rule, value, callback) => {
|
||
if (
|
||
!isWorkflowType.value &&
|
||
entity.value.authType === 'apiKey' &&
|
||
!value
|
||
) {
|
||
callback(new Error($t('message.required')));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
tokenKey: [
|
||
{
|
||
validator: (_rule, value, callback) => {
|
||
if (
|
||
!isWorkflowType.value &&
|
||
entity.value.authType === 'apiKey' &&
|
||
!value
|
||
) {
|
||
callback(new Error($t('message.required')));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
tokenValue: [
|
||
{
|
||
validator: (_rule, value, callback) => {
|
||
if (
|
||
!isWorkflowType.value &&
|
||
entity.value.authType === 'apiKey' &&
|
||
!value
|
||
) {
|
||
callback(new Error($t('message.required')));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
}));
|
||
|
||
watch(
|
||
() => entity.value.type,
|
||
(type) => {
|
||
if (Number(type || 1) === 2) {
|
||
entity.value.baseUrl = '';
|
||
entity.value.authType = 'none';
|
||
entity.value.position = '';
|
||
entity.value.tokenKey = '';
|
||
entity.value.tokenValue = '';
|
||
tempAddHeaders.value = [];
|
||
return;
|
||
}
|
||
entity.value.workflowId = '';
|
||
entity.value.workflowTitle = '';
|
||
entity.value.available = true;
|
||
entity.value.reasonMessage = '';
|
||
},
|
||
);
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([loadCategories(), loadWorkflowCandidates()]);
|
||
});
|
||
|
||
defineExpose({
|
||
openDialog,
|
||
});
|
||
|
||
function createDefaultEntity() {
|
||
return {
|
||
alias: '',
|
||
authType: 'none',
|
||
available: true,
|
||
baseUrl: '',
|
||
categoryIds: [],
|
||
deptId: '',
|
||
description: '',
|
||
englishName: '',
|
||
headers: '',
|
||
icon: '',
|
||
name: '',
|
||
pluginType: 1,
|
||
position: '',
|
||
reasonMessage: '',
|
||
title: '',
|
||
tokenKey: '',
|
||
tokenValue: '',
|
||
type: 1,
|
||
workflowId: '',
|
||
workflowTitle: '',
|
||
};
|
||
}
|
||
|
||
async function loadCategories() {
|
||
const res = await api.get('/api/v1/pluginCategory/list');
|
||
if (res.errorCode === 0) {
|
||
categoryList.value = res.data;
|
||
}
|
||
}
|
||
|
||
async function loadWorkflowCandidates(keyword: string = '') {
|
||
const res = await api.get('/api/v1/plugin/workflowCandidates', {
|
||
params: {
|
||
keyword,
|
||
},
|
||
});
|
||
if (res.errorCode === 0) {
|
||
workflowCandidates.value = Array.isArray(res.data) ? res.data : [];
|
||
ensureCurrentWorkflowOption();
|
||
}
|
||
}
|
||
|
||
function ensureCurrentWorkflowOption() {
|
||
if (!entity.value.workflowId || !entity.value.workflowTitle) {
|
||
return;
|
||
}
|
||
const exists = workflowCandidates.value.some(
|
||
(item) => String(item.id) === String(entity.value.workflowId),
|
||
);
|
||
if (!exists) {
|
||
workflowCandidates.value.unshift({
|
||
id: entity.value.workflowId,
|
||
title: entity.value.workflowTitle,
|
||
});
|
||
}
|
||
}
|
||
|
||
function openDialog(row: any) {
|
||
tempAddHeaders.value = row.headers ? JSON.parse(row.headers) : [];
|
||
isAdd.value = !row.id;
|
||
entity.value = {
|
||
...createDefaultEntity(),
|
||
...row,
|
||
authType: row.authType || 'none',
|
||
categoryIds: row.categoryIds?.map((item: any) => item.id) || [],
|
||
type: Number(row.type || row.pluginType || 1),
|
||
};
|
||
ensureCurrentWorkflowOption();
|
||
dialogVisible.value = true;
|
||
}
|
||
|
||
async function syncPluginCategories(pluginId: string, categoryIds: string[]) {
|
||
if (!pluginId) {
|
||
return;
|
||
}
|
||
const relationRes = await api.post(
|
||
'/api/v1/pluginCategoryMapping/updateRelation',
|
||
{
|
||
pluginId,
|
||
categoryIds,
|
||
},
|
||
);
|
||
if (relationRes.errorCode !== 0) {
|
||
throw new Error(relationRes.message || 'sync categories failed');
|
||
}
|
||
}
|
||
|
||
function normalizePayload() {
|
||
const plainEntity = { ...entity.value };
|
||
const categoryIds = [...(plainEntity.categoryIds || [])];
|
||
delete plainEntity.categoryIds;
|
||
if (isWorkflowType.value) {
|
||
plainEntity.baseUrl = '';
|
||
plainEntity.headers = [];
|
||
plainEntity.authType = 'none';
|
||
plainEntity.position = '';
|
||
plainEntity.tokenKey = '';
|
||
plainEntity.tokenValue = '';
|
||
}
|
||
return {
|
||
payload: {
|
||
...plainEntity,
|
||
headers: isWorkflowType.value ? [] : [...tempAddHeaders.value],
|
||
},
|
||
categoryIds,
|
||
};
|
||
}
|
||
|
||
function save() {
|
||
saveForm.value?.validate(async (valid) => {
|
||
if (!valid) {
|
||
return;
|
||
}
|
||
btnLoading.value = true;
|
||
const { payload, categoryIds } = normalizePayload();
|
||
const requestUrl = isAdd.value
|
||
? '/api/v1/plugin/plugin/save'
|
||
: '/api/v1/plugin/plugin/update';
|
||
try {
|
||
const res = await api.post(requestUrl, payload);
|
||
if (res.errorCode !== 0) {
|
||
ElMessage.error(res.message);
|
||
return;
|
||
}
|
||
const pluginId = res.data?.id || payload.id || entity.value.id;
|
||
if (!pluginId) {
|
||
throw new Error('插件保存成功,但未返回插件ID');
|
||
}
|
||
await syncPluginCategories(pluginId, categoryIds);
|
||
dialogVisible.value = false;
|
||
ElMessage.success(
|
||
isAdd.value ? $t('message.saveOkMessage') : $t('message.updateOkMessage'),
|
||
);
|
||
emit('reload');
|
||
} catch (error: any) {
|
||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||
} finally {
|
||
btnLoading.value = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
function closeDialog() {
|
||
saveForm.value?.resetFields();
|
||
isAdd.value = true;
|
||
tempAddHeaders.value = [];
|
||
entity.value = createDefaultEntity();
|
||
dialogVisible.value = false;
|
||
}
|
||
|
||
function addHeader() {
|
||
tempAddHeaders.value.push({
|
||
label: '',
|
||
value: '',
|
||
});
|
||
}
|
||
|
||
function removeHeader(index: number) {
|
||
tempAddHeaders.value.splice(index, 1);
|
||
}
|
||
|
||
function handleWorkflowChange(value: string) {
|
||
const target = workflowCandidates.value.find(
|
||
(item) => String(item.id) === String(value),
|
||
);
|
||
entity.value.workflowTitle = target?.title || '';
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<EasyFlowFormModal
|
||
v-model:open="dialogVisible"
|
||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||
:before-close="closeDialog"
|
||
align-center
|
||
:confirm-loading="btnLoading"
|
||
:confirm-text="$t('button.save')"
|
||
:submitting="btnLoading"
|
||
width="lg"
|
||
@confirm="save"
|
||
>
|
||
<ElForm
|
||
ref="saveForm"
|
||
:model="entity"
|
||
status-icon
|
||
:rules="rules"
|
||
label-position="top"
|
||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||
>
|
||
<ElFormItem
|
||
prop="icon"
|
||
:label="$t('plugin.icon')"
|
||
style="display: flex; align-items: center"
|
||
>
|
||
<UploadAvatar v-model="entity.icon" />
|
||
</ElFormItem>
|
||
<ElFormItem prop="type" :label="$t('plugin.type')">
|
||
<ElSelect v-model="entity.type" :disabled="!isAdd">
|
||
<ElOption
|
||
v-for="item in pluginTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem prop="name" :label="$t('plugin.name')">
|
||
<ElInput
|
||
v-model.trim="entity.name"
|
||
:placeholder="$t('plugin.placeholder.name')"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem prop="description" :label="$t('plugin.description')">
|
||
<ElInput
|
||
v-model.trim="entity.description"
|
||
:rows="4"
|
||
type="textarea"
|
||
:placeholder="$t('plugin.placeholder.description')"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem prop="categoryIds" :label="$t('plugin.category')">
|
||
<ElSelect
|
||
v-model="entity.categoryIds"
|
||
multiple
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
:max-collapse-tags="3"
|
||
>
|
||
<ElOption
|
||
v-for="item in categoryList"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<template v-if="isWorkflowType">
|
||
<ElAlert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
class="mb-4"
|
||
:title="$t('plugin.workflowPluginHint')"
|
||
/>
|
||
<ElFormItem prop="workflowId" :label="$t('plugin.workflowId')">
|
||
<ElSelect
|
||
v-model="entity.workflowId"
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadWorkflowCandidates"
|
||
:placeholder="$t('plugin.placeholder.workflow')"
|
||
@change="handleWorkflowChange"
|
||
>
|
||
<ElOption
|
||
v-for="item in workflowCandidates"
|
||
:key="item.id"
|
||
:label="item.title"
|
||
:value="item.id"
|
||
/>
|
||
</ElSelect>
|
||
<div class="form-helper-text">
|
||
{{ $t('plugin.onlyPublishedWorkflow') }}
|
||
</div>
|
||
</ElFormItem>
|
||
<ElAlert
|
||
v-if="!entity.available && entity.reasonMessage"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
:title="$t('plugin.workflowPluginUnavailable')"
|
||
:description="entity.reasonMessage"
|
||
/>
|
||
</template>
|
||
<template v-else>
|
||
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
||
<ElInput v-model.trim="entity.baseUrl" />
|
||
</ElFormItem>
|
||
<ElFormItem prop="Headers" label="Headers">
|
||
<div
|
||
v-for="(item, index) in tempAddHeaders"
|
||
:key="index"
|
||
class="headers-container-reduce flex flex-row gap-4"
|
||
>
|
||
<div class="head-con-content flex flex-row gap-4">
|
||
<ElInput v-model.trim="item.label" placeholder="header name" />
|
||
<ElInput v-model.trim="item.value" placeholder="header value" />
|
||
<ElIcon
|
||
size="20"
|
||
style="cursor: pointer"
|
||
@click="removeHeader(index)"
|
||
>
|
||
<Remove />
|
||
</ElIcon>
|
||
</div>
|
||
</div>
|
||
<ElButton class="addHeadersBtn" @click="addHeader">
|
||
<ElIcon size="18" style="margin-right: 4px">
|
||
<Plus />
|
||
</ElIcon>
|
||
{{ $t('button.add') }}headers
|
||
</ElButton>
|
||
</ElFormItem>
|
||
<ElFormItem prop="authType" :label="$t('plugin.authType')">
|
||
<ElSelect v-model="entity.authType">
|
||
<ElOption
|
||
v-for="item in authTypeList"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value || ''"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="entity.authType === 'apiKey'"
|
||
prop="position"
|
||
:label="$t('plugin.position')"
|
||
>
|
||
<ElRadioGroup v-model="entity.position">
|
||
<ElRadio value="headers">headers</ElRadio>
|
||
<ElRadio value="query">query</ElRadio>
|
||
</ElRadioGroup>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="entity.authType === 'apiKey'"
|
||
prop="tokenKey"
|
||
:label="$t('plugin.tokenKey')"
|
||
>
|
||
<ElInput v-model.trim="entity.tokenKey" />
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="entity.authType === 'apiKey'"
|
||
prop="tokenValue"
|
||
:label="$t('plugin.tokenValue')"
|
||
>
|
||
<ElInput v-model.trim="entity.tokenValue" />
|
||
</ElFormItem>
|
||
</template>
|
||
</ElForm>
|
||
</EasyFlowFormModal>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.headers-container-reduce {
|
||
align-items: center;
|
||
}
|
||
|
||
.addHeadersBtn {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
border-color: var(--el-color-primary);
|
||
border-style: dashed;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.head-con-content {
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-helper-text {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
</style>
|