2026-06-20 22:21:34 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
:open="visible"
|
|
|
|
|
|
:title="isEdit ? '编辑系统' : '新建系统'"
|
|
|
|
|
|
:width="720"
|
|
|
|
|
|
:mask-closable="false"
|
|
|
|
|
|
:destroy-on-close="true"
|
|
|
|
|
|
@cancel="handleCancel"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-steps :current="currentStep" size="small" class="es-system-form__steps">
|
|
|
|
|
|
<a-step title="基础信息" />
|
|
|
|
|
|
<a-step title="连接配置" />
|
|
|
|
|
|
<a-step title="认证" />
|
|
|
|
|
|
<a-step title="治理" />
|
|
|
|
|
|
</a-steps>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="es-system-form__content">
|
|
|
|
|
|
<!-- 步骤 1:基础信息 -->
|
|
|
|
|
|
<div v-show="currentStep === 0" class="es-system-form__step">
|
2026-07-11 21:55:47 +08:00
|
|
|
|
<a-form ref="basicFormRef" :model="formData.basic" :rules="basicRules" layout="vertical">
|
2026-06-20 22:21:34 +08:00
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="Slug" name="slug">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="formData.basic.slug"
|
|
|
|
|
|
:disabled="isEdit"
|
|
|
|
|
|
placeholder="唯一标识,如 my_system"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="名称" name="name">
|
|
|
|
|
|
<a-input v-model:value="formData.basic.name" placeholder="系统名称" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="分类" name="category">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="formData.basic.category"
|
|
|
|
|
|
placeholder="请选择分类"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
:options="categoryOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="适配器类型" name="adapter_type">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="formData.basic.adapter_type"
|
|
|
|
|
|
placeholder="请选择适配器"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
:options="adapterTypeOptions"
|
|
|
|
|
|
@change="handleAdapterTypeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="数据源" name="source_type">
|
2026-07-18 04:53:42 +08:00
|
|
|
|
<a-auto-complete
|
|
|
|
|
|
v-model:value="formData.basic.source_type"
|
|
|
|
|
|
:options="sourceTypeOptions"
|
|
|
|
|
|
placeholder="数据源标识,如 salesforce"
|
|
|
|
|
|
:filter-option="(input, option) => option.value.toLowerCase().includes(input.toLowerCase())"
|
|
|
|
|
|
@change="handleSourceTypeChange"
|
|
|
|
|
|
/>
|
2026-06-20 22:21:34 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="超时(秒)" name="timeout">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.basic.timeout"
|
|
|
|
|
|
:min="1"
|
|
|
|
|
|
:max="300"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
placeholder="1-300"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-form-item label="描述" name="description">
|
2026-07-11 21:55:47 +08:00
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="formData.basic.description"
|
|
|
|
|
|
:rows="3"
|
|
|
|
|
|
placeholder="系统描述"
|
|
|
|
|
|
/>
|
2026-06-20 22:21:34 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item name="enabled">
|
|
|
|
|
|
<a-switch v-model:checked="formData.basic.enabled" />
|
|
|
|
|
|
<span class="es-system-form__switch-label">启用系统</span>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 步骤 2:连接配置 -->
|
|
|
|
|
|
<div v-show="currentStep === 1" class="es-system-form__step">
|
|
|
|
|
|
<a-empty
|
|
|
|
|
|
v-if="!formData.basic.adapter_type"
|
|
|
|
|
|
description="请先在「基础信息」步骤选择适配器类型"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<LoadingState v-else-if="adapterSchemaLoading" type="skeleton" :rows="6" />
|
2026-07-18 04:53:42 +08:00
|
|
|
|
<div v-else-if="mergedAdapterSchema">
|
2026-07-11 21:55:47 +08:00
|
|
|
|
<p class="es-system-form__hint">适配器「{{ formData.basic.adapter_type }}」连接配置:</p>
|
2026-06-20 22:21:34 +08:00
|
|
|
|
<DynamicForm
|
|
|
|
|
|
v-model="formData.connection_config"
|
2026-07-18 04:53:42 +08:00
|
|
|
|
:schema="mergedAdapterSchema"
|
2026-06-20 22:21:34 +08:00
|
|
|
|
:hide-actions="true"
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-empty v-else description="该适配器未提供配置 Schema" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 步骤 3:认证 -->
|
|
|
|
|
|
<div v-show="currentStep === 2" class="es-system-form__step">
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<a-form-item label="认证类型">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="formData.auth_type"
|
|
|
|
|
|
placeholder="请选择认证类型"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
:options="authTypeOptions"
|
|
|
|
|
|
@change="handleAuthTypeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
2026-07-11 21:55:47 +08:00
|
|
|
|
<a-empty v-if="!formData.auth_type" description="请选择认证类型(可选)" />
|
2026-06-20 22:21:34 +08:00
|
|
|
|
<LoadingState v-else-if="authSchemaLoading" type="skeleton" :rows="6" />
|
|
|
|
|
|
<div v-else-if="authSchemaData">
|
|
|
|
|
|
<p class="es-system-form__hint">认证配置:</p>
|
|
|
|
|
|
<DynamicForm
|
|
|
|
|
|
v-model="formData.auth_config"
|
|
|
|
|
|
:schema="authSchemaData"
|
|
|
|
|
|
:hide-actions="true"
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-empty v-else description="该认证类型未提供配置 Schema" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 步骤 4:治理 -->
|
|
|
|
|
|
<div v-show="currentStep === 3" class="es-system-form__step">
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<a-divider orientation="left" plain>限流</a-divider>
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="请求数/秒">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.governance.rate_limit.requests_per_second"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
placeholder="不限流请留空"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="最大并发">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.governance.rate_limit.max_concurrent"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
placeholder="不限流请留空"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="left" plain>熔断器</a-divider>
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="失败阈值">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.governance.circuit_breaker.failure_threshold"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="恢复超时(秒)">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.governance.circuit_breaker.recovery_timeout"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="left" plain>连接池</a-divider>
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="最大连接数">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.governance.pool_config.max_size"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="最小空闲">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.governance.pool_config.min_idle"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="left" plain>可观测性</a-divider>
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="8">
|
|
|
|
|
|
<a-form-item label="启用追踪">
|
|
|
|
|
|
<a-switch v-model:checked="formData.governance.observability.tracing_enabled" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="8">
|
|
|
|
|
|
<a-form-item label="启用指标">
|
|
|
|
|
|
<a-switch v-model:checked="formData.governance.observability.metrics_enabled" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="8">
|
|
|
|
|
|
<a-form-item label="启用日志">
|
|
|
|
|
|
<a-switch v-model:checked="formData.governance.observability.logging_enabled" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="left" plain>重试策略</a-divider>
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="最大重试次数">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formData.governance.retry_policy.max_attempts"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
:max="10"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="退避策略">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="formData.governance.retry_policy.backoff_strategy"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
:options="backoffOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
|
|
<a-divider orientation="left" plain>密钥引用</a-divider>
|
|
|
|
|
|
<a-form-item label="密钥引用(JSON,可选)">
|
|
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="secretRefsText"
|
|
|
|
|
|
:rows="4"
|
|
|
|
|
|
placeholder='JSON 格式,如:{"db_password": "secret://db/pwd"}'
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<a-button @click="handleCancel">取消</a-button>
|
|
|
|
|
|
<a-button v-if="currentStep > 0" @click="handlePrev">上一步</a-button>
|
|
|
|
|
|
<a-button v-if="currentStep < 3" type="primary" @click="handleNext">下一步</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-else
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:loading="submitting"
|
|
|
|
|
|
:disabled="submitting"
|
|
|
|
|
|
@click="handleSubmit"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isEdit ? '保存' : '创建' }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, reactive, computed, watch } from 'vue'
|
|
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
import { useAdapterMetadata } from '@/composables/external-systems/useAdapterMetadata'
|
|
|
|
|
|
import { useAuthPlugin } from '@/composables/external-systems/useAuthPlugin'
|
2026-07-18 04:53:42 +08:00
|
|
|
|
import { useVendorIntegration } from '@/composables/external-systems/useVendorIntegration'
|
|
|
|
|
|
import { unwrap } from '@/composables/external-systems/utils'
|
|
|
|
|
|
import { getVendorConnectionSchemaApi } from '@/apis/external-systems/vendor_integration_api'
|
2026-07-14 15:13:29 +08:00
|
|
|
|
import { SYSTEM_CATEGORY_MAP as categoryDisplayMap } from '@/composables/external-systems/constants'
|
2026-06-20 22:21:34 +08:00
|
|
|
|
import DynamicForm from '@/components/external-systems/common/DynamicForm.vue'
|
|
|
|
|
|
import LoadingState from '@/components/external-systems/common/LoadingState.vue'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
visible: { type: Boolean, default: false },
|
|
|
|
|
|
systemId: { type: [String, Number], default: null },
|
2026-07-14 15:13:29 +08:00
|
|
|
|
initialData: { type: Object, default: null },
|
2026-07-18 04:53:42 +08:00
|
|
|
|
initialVendorMeta: { type: Object, default: null },
|
2026-07-14 15:13:29 +08:00
|
|
|
|
submitting: { type: Boolean, default: false }
|
2026-06-20 22:21:34 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-14 15:13:29 +08:00
|
|
|
|
const emit = defineEmits(['update:visible', 'submit'])
|
2026-06-20 22:21:34 +08:00
|
|
|
|
|
|
|
|
|
|
// ===== Composables =====
|
|
|
|
|
|
const {
|
|
|
|
|
|
schema: adapterSchemaRef,
|
|
|
|
|
|
schemaLoading: adapterSchemaLoading,
|
|
|
|
|
|
fetchSchema: fetchAdapterSchema,
|
2026-07-11 21:55:47 +08:00
|
|
|
|
resetSchema: resetAdapterSchema,
|
|
|
|
|
|
overview: adapterOverview,
|
2026-07-18 04:53:42 +08:00
|
|
|
|
fetchOverview: fetchAdapterOverview,
|
|
|
|
|
|
authPlugins,
|
|
|
|
|
|
fetchAuthPlugins,
|
|
|
|
|
|
resetAuthPlugins
|
2026-06-20 22:21:34 +08:00
|
|
|
|
} = useAdapterMetadata()
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
detailSchema: authSchemaState,
|
|
|
|
|
|
fetchDetailSchema: fetchAuthSchema,
|
|
|
|
|
|
resetDetailSchema: resetAuthSchema
|
|
|
|
|
|
} = useAuthPlugin()
|
|
|
|
|
|
|
2026-07-18 04:53:42 +08:00
|
|
|
|
const { sourceTypes, fetchListMeta } = useVendorIntegration()
|
|
|
|
|
|
|
2026-06-20 22:21:34 +08:00
|
|
|
|
// ===== Reactive State =====
|
|
|
|
|
|
const currentStep = ref(0)
|
|
|
|
|
|
const basicFormRef = ref(null)
|
|
|
|
|
|
const authSchemaLoading = ref(false)
|
|
|
|
|
|
const secretRefsText = ref('')
|
2026-07-18 04:53:42 +08:00
|
|
|
|
const vendorSupportedAuthTypes = ref(null)
|
|
|
|
|
|
const vendorExtraSchema = ref(null)
|
2026-06-20 22:21:34 +08:00
|
|
|
|
|
|
|
|
|
|
const formData = reactive({
|
|
|
|
|
|
basic: {
|
|
|
|
|
|
slug: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
category: undefined,
|
|
|
|
|
|
adapter_type: undefined,
|
|
|
|
|
|
source_type: '',
|
|
|
|
|
|
timeout: 30,
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
enabled: true
|
|
|
|
|
|
},
|
|
|
|
|
|
connection_config: {},
|
|
|
|
|
|
auth_type: undefined,
|
|
|
|
|
|
auth_config: {},
|
|
|
|
|
|
// 治理配置:6 项,对齐后端 CreateSystemRequest 顶级字段
|
|
|
|
|
|
governance: {
|
|
|
|
|
|
rate_limit: {
|
|
|
|
|
|
requests_per_second: undefined,
|
|
|
|
|
|
max_concurrent: undefined
|
|
|
|
|
|
},
|
|
|
|
|
|
circuit_breaker: {
|
|
|
|
|
|
failure_threshold: undefined,
|
|
|
|
|
|
recovery_timeout: undefined
|
|
|
|
|
|
},
|
|
|
|
|
|
pool_config: {
|
|
|
|
|
|
max_size: undefined,
|
|
|
|
|
|
min_idle: undefined
|
|
|
|
|
|
},
|
|
|
|
|
|
observability: {
|
|
|
|
|
|
tracing_enabled: false,
|
|
|
|
|
|
metrics_enabled: false,
|
|
|
|
|
|
logging_enabled: false
|
|
|
|
|
|
},
|
|
|
|
|
|
retry_policy: {
|
|
|
|
|
|
max_attempts: undefined,
|
|
|
|
|
|
backoff_strategy: undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Computed =====
|
|
|
|
|
|
const isEdit = computed(() => !!props.systemId)
|
|
|
|
|
|
const adapterSchemaData = computed(() => adapterSchemaRef.value)
|
2026-07-18 04:53:42 +08:00
|
|
|
|
const authSchemaData = computed(() => authSchemaState.data?.schema)
|
|
|
|
|
|
const mergedAdapterSchema = computed(() =>
|
|
|
|
|
|
mergeSchemas(adapterSchemaRef.value, vendorExtraSchema.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
const sourceTypeOptions = computed(() => {
|
|
|
|
|
|
const items = sourceTypes.value || []
|
|
|
|
|
|
return items.map((s) => ({ label: s.source_type, value: s.source_type }))
|
|
|
|
|
|
})
|
2026-06-20 22:21:34 +08:00
|
|
|
|
|
|
|
|
|
|
// ===== Constants =====
|
|
|
|
|
|
|
2026-07-11 21:55:47 +08:00
|
|
|
|
const categoryOptions = Object.entries(categoryDisplayMap).map(([value, label]) => ({
|
|
|
|
|
|
label,
|
|
|
|
|
|
value
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
// 适配器类型动态加载(对齐列表页行为)
|
|
|
|
|
|
const adapterTypeOptions = computed(() => {
|
|
|
|
|
|
const items = adapterOverview.value?.items || []
|
|
|
|
|
|
if (items.length) {
|
|
|
|
|
|
return items.map((item) => ({
|
|
|
|
|
|
label: item.display_name || item.adapter_type,
|
|
|
|
|
|
value: item.adapter_type
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
// fallback:概览未加载完成时提供常见类型
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: 'rest', value: 'rest' },
|
|
|
|
|
|
{ label: 'soap', value: 'soap' },
|
|
|
|
|
|
{ label: 'jdbc', value: 'jdbc' },
|
|
|
|
|
|
{ label: 'grpc', value: 'grpc' }
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
2026-06-20 22:21:34 +08:00
|
|
|
|
|
2026-07-18 04:53:42 +08:00
|
|
|
|
const authTypeOptions = computed(() => {
|
|
|
|
|
|
let items = authPlugins.value.map((p) => ({ label: p.display_name, value: p.type }))
|
|
|
|
|
|
if (vendorSupportedAuthTypes.value) {
|
|
|
|
|
|
const allowed = vendorSupportedAuthTypes.value
|
|
|
|
|
|
items = items.filter((i) => allowed.includes(i.value))
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!items.some((i) => i.value === 'none')) {
|
|
|
|
|
|
items.unshift({ label: '无认证', value: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
return items
|
|
|
|
|
|
})
|
2026-06-20 22:21:34 +08:00
|
|
|
|
|
|
|
|
|
|
const backoffOptions = [
|
|
|
|
|
|
{ label: '固定间隔', value: 'fixed' },
|
|
|
|
|
|
{ label: '线性退避', value: 'linear' },
|
|
|
|
|
|
{ label: '指数退避', value: 'exponential' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const basicRules = {
|
|
|
|
|
|
slug: [
|
|
|
|
|
|
{ required: true, message: '请输入 Slug' },
|
|
|
|
|
|
{
|
|
|
|
|
|
pattern: /^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$/,
|
|
|
|
|
|
message: '以字母或下划线开头,仅含字母、数字、下划线、连字符,长度 1-128'
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
name: [
|
|
|
|
|
|
{ required: true, message: '请输入名称' },
|
|
|
|
|
|
{ max: 128, message: '名称长度不能超过 128' }
|
|
|
|
|
|
],
|
2026-07-11 21:55:47 +08:00
|
|
|
|
timeout: [{ type: 'number', min: 1, max: 300, message: '超时范围 1-300 秒' }]
|
2026-06-20 22:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Methods =====
|
|
|
|
|
|
function resetForm() {
|
|
|
|
|
|
formData.basic = {
|
|
|
|
|
|
slug: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
category: undefined,
|
|
|
|
|
|
adapter_type: undefined,
|
|
|
|
|
|
source_type: '',
|
|
|
|
|
|
timeout: 30,
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
enabled: true
|
|
|
|
|
|
}
|
|
|
|
|
|
formData.connection_config = {}
|
|
|
|
|
|
formData.auth_type = undefined
|
|
|
|
|
|
formData.auth_config = {}
|
|
|
|
|
|
formData.governance = {
|
|
|
|
|
|
rate_limit: { requests_per_second: undefined, max_concurrent: undefined },
|
|
|
|
|
|
circuit_breaker: { failure_threshold: undefined, recovery_timeout: undefined },
|
|
|
|
|
|
pool_config: { max_size: undefined, min_idle: undefined },
|
|
|
|
|
|
observability: { tracing_enabled: false, metrics_enabled: false, logging_enabled: false },
|
|
|
|
|
|
retry_policy: { max_attempts: undefined, backoff_strategy: undefined }
|
|
|
|
|
|
}
|
|
|
|
|
|
secretRefsText.value = ''
|
|
|
|
|
|
currentStep.value = 0
|
|
|
|
|
|
resetAdapterSchema()
|
|
|
|
|
|
resetAuthSchema()
|
2026-07-18 04:53:42 +08:00
|
|
|
|
resetAuthPlugins()
|
|
|
|
|
|
vendorSupportedAuthTypes.value = null
|
|
|
|
|
|
vendorExtraSchema.value = null
|
2026-06-20 22:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fillForm(data) {
|
|
|
|
|
|
if (!data) return
|
|
|
|
|
|
formData.basic.slug = data.slug || ''
|
|
|
|
|
|
formData.basic.name = data.name || ''
|
|
|
|
|
|
formData.basic.category = data.category
|
|
|
|
|
|
formData.basic.adapter_type = data.adapter_type
|
|
|
|
|
|
formData.basic.source_type = data.source_type || ''
|
|
|
|
|
|
formData.basic.timeout = data.timeout ?? 30
|
|
|
|
|
|
formData.basic.description = data.description || ''
|
|
|
|
|
|
formData.basic.enabled = data.enabled ?? true
|
|
|
|
|
|
formData.connection_config = data.connection_config || {}
|
|
|
|
|
|
formData.auth_type = data.auth_type
|
|
|
|
|
|
formData.auth_config = data.auth_config || {}
|
|
|
|
|
|
if (data.rate_limit) {
|
|
|
|
|
|
formData.governance.rate_limit = { ...data.rate_limit }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.circuit_breaker) {
|
|
|
|
|
|
formData.governance.circuit_breaker = { ...data.circuit_breaker }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.pool_config) {
|
|
|
|
|
|
formData.governance.pool_config = { ...data.pool_config }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.observability) {
|
|
|
|
|
|
formData.governance.observability = { ...data.observability }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.retry_policy) {
|
|
|
|
|
|
formData.governance.retry_policy = { ...data.retry_policy }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.secret_refs) {
|
|
|
|
|
|
secretRefsText.value = JSON.stringify(data.secret_refs, null, 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAdapterSchema(type) {
|
|
|
|
|
|
if (!type) {
|
|
|
|
|
|
resetAdapterSchema()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// schemaLoading 由 useAdapterMetadata.fetchSchema 内部维护
|
|
|
|
|
|
await fetchAdapterSchema(type)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAuthSchema(authType, adapterType) {
|
|
|
|
|
|
if (!authType) {
|
|
|
|
|
|
resetAuthSchema()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
authSchemaLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fetchAuthSchema(authType, adapterType)
|
2026-07-18 04:53:42 +08:00
|
|
|
|
if (authSchemaState.data?.schema) {
|
|
|
|
|
|
authSchemaState.data = {
|
|
|
|
|
|
...authSchemaState.data,
|
|
|
|
|
|
schema: normalizeAuthSchema(authSchemaState.data.schema)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-20 22:21:34 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
authSchemaLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 04:53:42 +08:00
|
|
|
|
async function loadVendorExtraSchema(sourceType) {
|
|
|
|
|
|
if (!sourceType) {
|
|
|
|
|
|
vendorExtraSchema.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = unwrap(await getVendorConnectionSchemaApi(sourceType))
|
|
|
|
|
|
vendorExtraSchema.value = data?.connection_extra_schema || null
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 404 等错误静默处理:该厂商无额外连接配置
|
|
|
|
|
|
vendorExtraSchema.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mergeSchemas(base, extra) {
|
|
|
|
|
|
if (!base) return extra
|
|
|
|
|
|
if (!extra) return base
|
|
|
|
|
|
return {
|
|
|
|
|
|
...base,
|
|
|
|
|
|
properties: { ...(base.properties || {}), ...(extra.properties || {}) },
|
|
|
|
|
|
required: [...new Set([...(base.required || []), ...(extra.required || [])])]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeAuthSchema(schema) {
|
|
|
|
|
|
if (!schema || !schema.properties) return schema
|
|
|
|
|
|
const normalized = { ...schema, properties: { ...schema.properties } }
|
|
|
|
|
|
for (const [name, field] of Object.entries(normalized.properties)) {
|
|
|
|
|
|
if (/private_key/i.test(name)) {
|
|
|
|
|
|
normalized.properties[name] = { ...field, format: 'multiline' }
|
|
|
|
|
|
} else if (!/_url$/.test(name) && /^(password|secret|token)$/i.test(name)) {
|
|
|
|
|
|
normalized.properties[name] = { ...field, format: 'password' }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 22:21:34 +08:00
|
|
|
|
async function handleAdapterTypeChange(value) {
|
|
|
|
|
|
formData.basic.adapter_type = value
|
|
|
|
|
|
formData.connection_config = {}
|
2026-07-18 04:53:42 +08:00
|
|
|
|
formData.auth_type = undefined
|
|
|
|
|
|
formData.auth_config = {}
|
|
|
|
|
|
await fetchAuthPlugins(value)
|
2026-06-20 22:21:34 +08:00
|
|
|
|
await loadAdapterSchema(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 04:53:42 +08:00
|
|
|
|
function handleSourceTypeChange(value) {
|
|
|
|
|
|
formData.connection_config = {}
|
|
|
|
|
|
loadVendorExtraSchema(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 22:21:34 +08:00
|
|
|
|
async function handleAuthTypeChange(value) {
|
|
|
|
|
|
formData.auth_type = value
|
|
|
|
|
|
formData.auth_config = {}
|
|
|
|
|
|
await loadAuthSchema(value, formData.basic.adapter_type)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleNext() {
|
|
|
|
|
|
if (currentStep.value === 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await basicFormRef.value?.validate()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 进入步骤 2 时按需加载适配器 Schema
|
|
|
|
|
|
if (formData.basic.adapter_type && !adapterSchemaData.value) {
|
|
|
|
|
|
await loadAdapterSchema(formData.basic.adapter_type)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentStep.value === 2 && formData.auth_type && !authSchemaData.value) {
|
|
|
|
|
|
await loadAuthSchema(formData.auth_type, formData.basic.adapter_type)
|
|
|
|
|
|
}
|
|
|
|
|
|
currentStep.value += 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePrev() {
|
|
|
|
|
|
currentStep.value -= 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildPayload() {
|
|
|
|
|
|
// 后端 CreateSystemRequest/UpdateSystemRequest 中治理配置是顶级字段
|
|
|
|
|
|
// rate_limit/circuit_breaker/pool_config/observability/retry_policy/secret_refs
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
...formData.basic,
|
|
|
|
|
|
connection_config: formData.connection_config,
|
|
|
|
|
|
auth_type: formData.auth_type || 'none',
|
|
|
|
|
|
auth_config: formData.auth_config
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 限流:仅包含有值的字段
|
|
|
|
|
|
const rl = formData.governance.rate_limit
|
|
|
|
|
|
if (rl.requests_per_second != null || rl.max_concurrent != null) {
|
|
|
|
|
|
payload.rate_limit = { ...rl }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 熔断器
|
|
|
|
|
|
const cb = formData.governance.circuit_breaker
|
|
|
|
|
|
if (cb.failure_threshold != null || cb.recovery_timeout != null) {
|
|
|
|
|
|
payload.circuit_breaker = { ...cb }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 连接池
|
|
|
|
|
|
const pc = formData.governance.pool_config
|
|
|
|
|
|
if (pc.max_size != null || pc.min_idle != null) {
|
|
|
|
|
|
payload.pool_config = { ...pc }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 可观测性:任一开关开启则提交
|
|
|
|
|
|
const obs = formData.governance.observability
|
|
|
|
|
|
if (obs.tracing_enabled || obs.metrics_enabled || obs.logging_enabled) {
|
|
|
|
|
|
payload.observability = { ...obs }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重试策略
|
|
|
|
|
|
const rp = formData.governance.retry_policy
|
|
|
|
|
|
if (rp.max_attempts != null || rp.backoff_strategy) {
|
|
|
|
|
|
payload.retry_policy = { ...rp }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 密钥引用:解析 JSON 文本
|
|
|
|
|
|
if (secretRefsText.value.trim()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
payload.secret_refs = JSON.parse(secretRefsText.value)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 校验在 handleSubmit 中处理,此处跳过
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return payload
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSubmit() {
|
|
|
|
|
|
// 密钥引用 JSON 校验(后端 secret_refs 为 dict[str, Any])
|
|
|
|
|
|
if (secretRefsText.value.trim()) {
|
|
|
|
|
|
let parsed
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsed = JSON.parse(secretRefsText.value)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('密钥引用必须为合法 JSON')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
|
|
|
|
message.error('密钥引用必须为 JSON 对象')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-14 15:13:29 +08:00
|
|
|
|
const payload = buildPayload()
|
|
|
|
|
|
emit('submit', {
|
|
|
|
|
|
mode: isEdit.value ? 'edit' : 'create',
|
|
|
|
|
|
id: props.systemId,
|
|
|
|
|
|
data: payload
|
|
|
|
|
|
})
|
2026-06-20 22:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleCancel() {
|
|
|
|
|
|
emit('update:visible', false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Watch =====
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.visible,
|
|
|
|
|
|
async (val) => {
|
|
|
|
|
|
if (val) {
|
|
|
|
|
|
resetForm()
|
2026-07-11 21:55:47 +08:00
|
|
|
|
// 加载适配器概览(动态填充适配器类型选项)
|
2026-07-18 04:53:42 +08:00
|
|
|
|
// adapterOverview 默认为空数组 [](truthy),必须用 .length 判断
|
|
|
|
|
|
if (!adapterOverview.value?.length) {
|
2026-07-11 21:55:47 +08:00
|
|
|
|
fetchAdapterOverview()
|
|
|
|
|
|
}
|
2026-07-18 04:53:42 +08:00
|
|
|
|
// authPlugins 无需预加载,等 adapter_type 选择后再 fetchAuthPlugins
|
|
|
|
|
|
// 加载厂商列表元数据(用于 source_type 自动补全)
|
|
|
|
|
|
if (!sourceTypes.value?.length) {
|
|
|
|
|
|
fetchListMeta()
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.initialVendorMeta) {
|
|
|
|
|
|
vendorSupportedAuthTypes.value = props.initialVendorMeta.supported_auth_types || null
|
|
|
|
|
|
fillForm({
|
|
|
|
|
|
source_type: props.initialVendorMeta.source_type,
|
|
|
|
|
|
adapter_type: props.initialVendorMeta.adapter_type
|
|
|
|
|
|
})
|
|
|
|
|
|
if (formData.basic.adapter_type) {
|
|
|
|
|
|
await loadAdapterSchema(formData.basic.adapter_type)
|
|
|
|
|
|
await fetchAuthPlugins(formData.basic.adapter_type)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (formData.basic.source_type) {
|
|
|
|
|
|
await loadVendorExtraSchema(formData.basic.source_type)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (props.initialData) {
|
2026-06-20 22:21:34 +08:00
|
|
|
|
fillForm(props.initialData)
|
|
|
|
|
|
if (formData.basic.adapter_type) {
|
|
|
|
|
|
await loadAdapterSchema(formData.basic.adapter_type)
|
2026-07-18 04:53:42 +08:00
|
|
|
|
await fetchAuthPlugins(formData.basic.adapter_type)
|
2026-06-20 22:21:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (formData.auth_type) {
|
|
|
|
|
|
await loadAuthSchema(formData.auth_type, formData.basic.adapter_type)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
.es-system-form {
|
|
|
|
|
|
&__steps {
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__content {
|
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
|
max-height: 480px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__step {
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__hint {
|
|
|
|
|
|
margin: 0 0 12px;
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__switch-label {
|
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
|
color: var(--gray-700);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|