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">
|
|
|
|
|
|
<a-input v-model:value="formData.basic.source_type" placeholder="数据源标识" />
|
|
|
|
|
|
</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" />
|
|
|
|
|
|
<div v-else-if="adapterSchemaData">
|
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"
|
|
|
|
|
|
:schema="adapterSchemaData"
|
|
|
|
|
|
: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-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 },
|
|
|
|
|
|
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,
|
|
|
|
|
|
fetchOverview: fetchAdapterOverview
|
2026-06-20 22:21:34 +08:00
|
|
|
|
} = useAdapterMetadata()
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
detailSchema: authSchemaState,
|
|
|
|
|
|
fetchDetailSchema: fetchAuthSchema,
|
|
|
|
|
|
resetDetailSchema: resetAuthSchema
|
|
|
|
|
|
} = useAuthPlugin()
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Reactive State =====
|
|
|
|
|
|
const currentStep = ref(0)
|
|
|
|
|
|
const basicFormRef = ref(null)
|
|
|
|
|
|
const authSchemaLoading = ref(false)
|
|
|
|
|
|
const secretRefsText = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
const authSchemaData = computed(() => authSchemaState.data)
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 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
|
|
|
|
|
|
|
|
|
|
const authTypeOptions = [
|
|
|
|
|
|
{ label: '无认证', value: 'none' },
|
|
|
|
|
|
{ label: 'Basic Auth', value: 'basic' },
|
|
|
|
|
|
{ label: 'Bearer Token', value: 'bearer' },
|
|
|
|
|
|
{ label: 'API Key', value: 'api_key' },
|
|
|
|
|
|
{ label: 'OAuth2', value: 'oauth2' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
authSchemaLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleAdapterTypeChange(value) {
|
|
|
|
|
|
formData.basic.adapter_type = value
|
|
|
|
|
|
formData.connection_config = {}
|
|
|
|
|
|
await loadAdapterSchema(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
// 加载适配器概览(动态填充适配器类型选项)
|
|
|
|
|
|
if (!adapterOverview.value) {
|
|
|
|
|
|
fetchAdapterOverview()
|
|
|
|
|
|
}
|
2026-06-20 22:21:34 +08:00
|
|
|
|
if (props.initialData) {
|
|
|
|
|
|
fillForm(props.initialData)
|
|
|
|
|
|
if (formData.basic.adapter_type) {
|
|
|
|
|
|
await loadAdapterSchema(formData.basic.adapter_type)
|
|
|
|
|
|
}
|
|
|
|
|
|
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>
|