Repository: APISIX-Unofficial-UI/apisix-dashboard-unofficial
Pull request #27: feat(upstream): Add upstream-form component
Author: login: iceboundx
Created: 2025-04-07T15:19:37Z | Merged: 2025-05-04T08:16:23Z

## Motivation
Configuration for APISIX Upstreams is a common requirement across various parts of the apisix-unofficial-dashboard, such as Routes and potentially Services in the future. To promote code reuse, maintain consistency, and simplify maintenance, this PR introduces a dedicated, reusable component (UpstreamForm) for handling upstream configurations.

## Changes

1.  **New Component (`UpstreamForm`):**
    *   Created `src/components/upstream/upstream-form.vue` which encapsulates the UI and logic for configuring APISIX upstream objects.
    *   Supports configuration of:
        *   Load Balancer Algorithm (`type`)
        *   Upstream Type selection (Static Nodes vs. Service Discovery)
        *   Node configuration (Host, Port, Weight) with dynamic add/remove functionality.
        *   Basic Service Discovery configuration (`discovery_type`, `service_name`, `pass_host`, `upstream_host`).
        *   Retry settings (`retries`, `retry_timeout`).
        *   Protocol (`scheme`).
        *   Timeout settings (`timeout.connect`, `timeout.send`, `timeout.read`).
        *   Connection Pool (`keepalive_pool`).
        *   Basic Active Health Check configuration (enable toggle, type, core parameters, healthy/unhealthy checks).
    *   Uses `tdesign-vue-next` components and includes basic validation.
    *   Accepts upstream data via `v-model` (`modelValue` prop).
    *   Exposes `validate()` and `getApiFormattedData()` methods for form interaction and retrieving data in the expected APISIX format.
    *   Exposes a `resetForm()` method.

2.  **Supporting Files:**
    *   `src/components/upstream/constants.ts`: Defines constants for select options (Load Balancer, Discovery Type, Protocol, Health Check Types) and validation rules.
    *   `src/components/upstream/upstream-form.less`: Contains styling specific to the form component.

3.  **Test Harness:**
    *   Added `src/components/upstream/upstream-form-harness.vue` as a development/testing page for the `UpstreamForm` component.
    *   This page can be accessed via the route `/#/dev/upstream-form-harness` (ensure routing is set up if needed).

## Limitations & Future Work

This PR establishes the foundational `UpstreamForm` component. However, certain features are intentionally **not fully implemented** in this initial version:

*   **Service Discovery:** While the basic fields are present, the full logic and potential variations might require further refinement.
*   **Health Checks:** Only active health checks are partially implemented. Passive health checks and more advanced options for active checks (e.g., specific headers, full status code handling nuances) are not yet included.

The plan is to merge this component first to establish the structure. The remaining features for Service Discovery and Health Checks will be fully implemented and tested in subsequent PRs, likely when integrating this component into the Router configuration module.

## Demo
![image](https://github.com/user-attachments/assets/d929e2cf-acd6-44b2-afbc-f8afeab3922b)
![image](https://github.com/user-attachments/assets/56bd0e12-64b9-49ef-b15b-3ef779c3e05b)
![image](https://github.com/user-attachments/assets/67c7dbf0-2392-4315-b3ae-b0a95a3c334f)
![image](https://github.com/user-attachments/assets/71505eec-34a3-4f60-89b9-049156e7d6e6)
![image](https://github.com/user-attachments/assets/9e586a7b-ac1f-40b4-a7dd-ee7a07eeaca0)



Diff:
diff --git a/src/components/number-tag-input/index.vue b/src/components/number-tag-input/index.vue
new file mode 100644
index 0000000..1e400ef
--- /dev/null
+++ b/src/components/number-tag-input/index.vue
@@ -0,0 +1,42 @@
+<script setup lang="ts">
+import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next';
+import { computed, WritableComputedRef } from 'vue';
+
+interface Props {
+  modelValue: number[];
+  placeholder?: string;
+  tagProps?: TagProps;
+  excessTagsDisplayType?: TagInputProps['excessTagsDisplayType'];
+  autoWidth?: boolean;
+}
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: () => [],
+  tagProps: () => ({ theme: 'primary', variant: 'light' }),
+  excessTagsDisplayType: 'scroll',
+  autoWidth: false,
+});
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: number[]): void;
+}>();
+
+const internalStringModel: WritableComputedRef<string[]> = computed({
+  get(): string[] {
+    return props.modelValue.map(String);
+  },
+  set(newValue: string[]): void {
+    const numbers = newValue.map((str) => parseInt(str, 10)).filter((num): num is number => !Number.isNaN(num));
+    emit('update:modelValue', numbers);
+  },
+});
+</script>
+
+<template>
+  <t-tag-input
+    v-model="internalStringModel"
+    :placeholder="placeholder"
+    :tag-props="tagProps"
+    :excess-tags-display-type="excessTagsDisplayType"
+    :auto-width="autoWidth"
+  />
+</template>
diff --git a/src/components/upstream/constants.ts b/src/components/upstream/constants.ts
new file mode 100644
index 0000000..8a12335
--- /dev/null
+++ b/src/components/upstream/constants.ts
@@ -0,0 +1,47 @@
+import { FormRule } from 'tdesign-vue-next';
+
+import { t } from '@/locales';
+
+export const LOAD_BALANCER_OPTIONS = [
+  { label: t('components.upstreamForm.loadBalancer.roundRobin'), value: 'roundrobin' },
+  { label: t('components.upstreamForm.loadBalancer.consistentHash'), value: 'chash' },
+  { label: t('components.upstreamForm.loadBalancer.ewma'), value: 'ewma' },
+  { label: t('components.upstreamForm.loadBalancer.leastConn'), value: 'least_conn' },
+];
+
+export const DISCOVERY_TYPE_OPTIONS = [
+  { label: 'DNS', value: 'dns' },
+  { label: 'Consul', value: 'consul' },
+  { label: 'Consul KV', value: 'consul_kv' },
+  { label: 'Kubernetes', value: 'kubernetes' },
+  { label: 'Nacos', value: 'nacos' },
+  { label: 'Eureka', value: 'eureka' },
+];
+
+export const PROTOCOL_OPTIONS = [
+  { label: 'HTTP', value: 'http' },
+  { label: 'HTTPS', value: 'https' },
+  { label: 'gRPC', value: 'grpc' },
+  { label: 'gRPCS', value: 'grpcs' },
+];
+
+export const ACTIVE_HEALTH_CHECK_TYPES = [
+  { label: 'HTTP', value: 'http' },
+  { label: 'HTTPS', value: 'https' },
+  { label: 'tcp', value: 'tcp' },
+];
+
+export const HEALTH_CHECK_RULES: Record<string, FormRule[]> = {
+  'checks.active.healthy.interval': [
+    { required: true, message: t('components.upstreamForm.healthCheck.active.healthy.intervalRequired') },
+  ],
+  'checks.active.healthy.successes': [
+    { required: true, message: t('components.upstreamForm.healthCheck.active.healthy.successesRequired') },
+  ],
+  'checks.active.unhealthy.interval': [
+    { required: true, message: t('components.upstreamForm.healthCheck.active.unhealthy.intervalRequired') },
+  ],
+  'checks.active.unhealthy.http_failures': [
+    { required: true, message: t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresRequired') },
+  ],
+};
diff --git a/src/components/upstream/upstream-form-harness.vue b/src/components/upstream/upstream-form-harness.vue
new file mode 100644
index 0000000..170cb8b
--- /dev/null
+++ b/src/components/upstream/upstream-form-harness.vue
@@ -0,0 +1,124 @@
+<template>
+  <div style="padding: 30px; max-width: 800px; margin: auto">
+    <h1>Upstream 表单测试页面</h1>
+
+    <t-card title="表单实例">
+      <upstream-form ref="upstreamFormRef" v-model="formData" />
+    </t-card>
+
+    <t-card title="操作" style="margin-top: 20px">
+      <t-space direction="vertical" style="width: 100%">
+        <t-space>
+          <t-button @click="loadInitialData('empty')">加载空数据</t-button>
+          <t-button @click="loadInitialData('nodes')">加载node数据</t-button>
+          <t-button theme="primary" @click="validateForm">校验表单</t-button>
+          <t-button theme="success" @click="showApiData">显示 API 格式数据</t-button>
+        </t-space>
+        <t-alert v-if="validationResult !== null" :theme="validationResult ? 'success' : 'error'">
+          校验结果: {{ validationResult ? '通过' : '失败' }}
+        </t-alert>
+      </t-space>
+    </t-card>
+    <t-card title="API 格式化数据 (getApiFormattedData)" style="margin-top: 20px">
+      <pre>{{ JSON.stringify(apiFormattedData, null, 2) }}</pre>
+    </t-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { MessagePlugin } from 'tdesign-vue-next';
+import { ref } from 'vue';
+
+import UpstreamForm from '@/components/upstream/upstream-form.vue';
+
+const upstreamFormRef = ref<InstanceType<typeof UpstreamForm> | null>(null);
+
+// --- 数据引用 ---
+const formData = ref<Record<string, any> | null>(null);
+const apiFormattedData = ref<Record<string, any> | null>(null); // 存储 getApiFormattedData 的结果
+const validationResult = ref<boolean | null>(null);
+
+// --- 方法 ---
+const loadInitialData = (type: 'nodes' | 'discovery' | 'empty') => {
+  validationResult.value = null;
+  apiFormattedData.value = null;
+  if (type === 'nodes') {
+    formData.value = {
+      type: 'chash',
+      pass_host: 'pass',
+      retries: 10,
+      retry_timeout: 5,
+      scheme: 'https',
+      timeout: {
+        connect: 151,
+        send: 151,
+        read: 152,
+      },
+      keepalive_pool: {
+        size: 3200,
+        idle_timeout: 601,
+        requests: 500,
+      },
+      checks: {
+        active: {
+          type: 'http',
+          http_path: '/v1/health_check',
+          host: '192.168.0.1',
+          healthy: {
+            interval: 10,
+            successes: 5,
+            http_statuses: [200, 302],
+          },
+          unhealthy: {
+            interval: 1,
+            http_failures: 3,
+            http_statuses: [429, 404, 500, 501, 502, 503, 504, 505],
+          },
+        },
+      },
+      nodes: {
+        '192.168.0.1:1233': 2,
+        '192.168.0.2:2333': 4,
+      },
+    };
+  } else if (type === 'discovery') {
+    formData.value = {
+      /* ... 服务发现示例数据 ... */
+    };
+  } else {
+    upstreamFormRef.value?.resetForm();
+  }
+  console.log('测试页面: 加载数据', formData.value);
+};
+
+const validateForm = async () => {
+  if (upstreamFormRef.value) {
+    const result = upstreamFormRef.value.validate();
+    if (result === false) {
+      validationResult.value = false;
+      MessagePlugin.error('校验失败');
+      console.error('测试页面: 校验失败 - 节点验证未通过');
+      return;
+    }
+    validationResult.value = true;
+    MessagePlugin.success('校验通过');
+    console.log('测试页面: 校验通过');
+  }
+};
+
+const showApiData = () => {
+  if (upstreamFormRef.value) {
+    apiFormattedData.value = upstreamFormRef.value.getApiFormattedData();
+    MessagePlugin.info('API 数据已打印到控制台并在下方显示。');
+  }
+};
+
+// 初始加载空状态
+loadInitialData('empty');
+</script>
+
+<style scoped>
+.t-card {
+  margin-bottom: 20px;
+}
+</style>
diff --git a/src/components/upstream/upstream-form.less b/src/components/upstream/upstream-form.less
new file mode 100644
index 0000000..48ae0b4
--- /dev/null
+++ b/src/components/upstream/upstream-form.less
@@ -0,0 +1,75 @@
+.node-item {
+  margin-bottom: var(--td-comp-margin-s);
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.node-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+  flex-wrap: nowrap;
+}
+
+.node-label,
+.remove-button {
+  flex: 0 0 auto;
+  margin-top: 4px;
+}
+
+.node-input {
+  flex: 1 1 auto;
+  min-width: 200px;
+}
+
+.node-input-port {
+  width: 70px;
+}
+
+.node-input-weight {
+  width: 110px; 
+}
+
+.node-input :deep(.t-input__inner),
+.node-input-number :deep(.t-input-number__input) {
+  width: 100%;
+}
+
+
+// Use t-row/t-col for better layout control than simple margin on unit
+:deep(.t-input-number__suffix) {
+  padding-left: var(--td-comp-paddingLR-s);
+}
+
+.t-divider {
+  margin-top: var(--td-comp-margin-xxl);
+  margin-bottom: var(--td-comp-margin-l);
+  &:first-of-type {
+    // No top margin for the very first divider
+    margin-top: 0;
+  }
+}
+
+:deep(.t-form__label--top) {
+  margin-bottom: var(--td-comp-margin-s);
+}
+
+:deep(.t-form__item) {
+  .t-input,
+  .t-select__wrap,
+  .t-input-number {
+    width: 100%; // Ensure inputs fill space within cols/items
+  }
+}
+
+// Align divider text left
+:deep(.t-divider--horizontal.t-divider--with-text-left) {
+  &::before {
+    width: 2%; // Small gap before left-aligned text
+  }
+  &::after {
+    width: 98%;
+  }
+}
diff --git a/src/components/upstream/upstream-form.vue b/src/components/upstream/upstream-form.vue
new file mode 100644
index 0000000..886a0d1
--- /dev/null
+++ b/src/components/upstream/upstream-form.vue
@@ -0,0 +1,762 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType" variant="primary-filled">
+        <t-radio-button value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio-button>
+        <t-radio-button value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio-button>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number node-input-port"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number node-input-weight"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-radio-group
+          v-model="localUpstreamData.pass_host"
+          variant="primary-filled"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-radio-button value="pass">
+            {{ t('components.upstreamForm.discovery.hostHeaderPass') }}
+          </t-radio-button>
+          <t-radio-button value="node">
+            {{ t('components.upstreamForm.discovery.hostHeaderNode') }}
+          </t-radio-button>
+          <t-radio-button value="rewrite"
+            >{{ t('components.upstreamForm.discovery.hostHeaderRewrite') }}
+          </t-radio-button>
+        </t-radio-group>
+      </t-form-item>
+      <t-form-item
+        v-if="localUpstreamData.pass_host === 'rewrite'"
+        :label="t('components.upstreamForm.discovery.upstreamHostLabel')"
+        name="upstream_host"
+      >
+        <t-input
+          v-model="localUpstreamData.upstream_host"
+          :placeholder="t('components.upstreamForm.discovery.upstreamHostPlaceholder')"
+        />
+      </t-form-item>
+    </div>
+
+    <!-- 3. Retries and Retry Timeout -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retries.label')" name="retries">
+          <t-input-number
+            v-model="localUpstreamData.retries"
+            :placeholder="t('components.upstreamForm.retries.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retryTimeout.label')" name="retry_timeout">
+          <t-input-number
+            v-model="localUpstreamData.retry_timeout"
+            :placeholder="t('components.upstreamForm.retryTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 4. Protocol -->
+    <t-form-item :label="t('components.upstreamForm.protocol.label')" name="scheme" required-mark>
+      <t-select v-model="localUpstreamData.scheme" :placeholder="t('components.upstreamForm.protocol.placeholder')">
+        <t-option v-for="item in PROTOCOL_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 5. Timeout Settings -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectTimeout.label')" name="timeout.connect">
+          <t-input-number
+            v-model="localUpstreamData.timeout.connect"
+            :placeholder="t('components.upstreamForm.connectTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.sendTimeout.label')" name="timeout.send">
+          <t-input-number
+            v-model="localUpstreamData.timeout.send"
+            :placeholder="t('components.upstreamForm.sendTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.receiveTimeout.label')" name="timeout.read">
+          <t-input-number
+            v-model="localUpstreamData.timeout.read"
+            :placeholder="t('components.upstreamForm.receiveTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 6. Connection Pool -->
+    <t-divider>{{ t('components.upstreamForm.connectionPool.title') }}</t-divider>
+    <t-row :gutter="[16, 16]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectionPool.capacity.label')" name="keepalive_pool.size">
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.size"
+            :placeholder="t('components.upstreamForm.connectionPool.capacity.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.idleTimeout.label')"
+          name="keepalive_pool.idle_timeout"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.idle_timeout"
+            :placeholder="t('components.upstreamForm.connectionPool.idleTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.maxRequests.label')"
+          name="keepalive_pool.requests"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.requests"
+            :placeholder="t('components.upstreamForm.connectionPool.maxRequests.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 7. Health Checks -->
+    <t-divider>{{ t('components.upstreamForm.healthCheck.title') }}</t-divider>
+    <t-form-item :label="t('components.upstreamForm.healthCheck.active.enableLabel')" name="healthCheckActiveEnabled">
+      <t-switch v-model="isHealthCheckActive" />
+    </t-form-item>
+
+    <div v-if="isHealthCheckActive">
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.healthCheckType')"
+        name="checks.active.type"
+        required-mark
+      >
+        <t-radio-group v-model="localUpstreamData.checks.active.type" variant="primary-filled">
+          <t-radio-button
+            v-for="item in ACTIVE_HEALTH_CHECK_TYPES"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+          >
+            {{ item.label }}
+          </t-radio-button>
+        </t-radio-group>
+      </t-form-item>
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.httpPathLabel')"
+        name="checks.active.http_path"
+        required-mark
+      >
+        <t-input
+          v-model="localUpstreamData.checks.active.http_path"
+          :placeholder="t('components.upstreamForm.healthCheck.active.httpPathPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.hostLabel')" name="checks.active.host">
+        <t-input
+          v-model="localUpstreamData.checks.active.host"
+          :placeholder="t('components.upstreamForm.healthCheck.active.hostPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.portLabel')" name="checks.active.port">
+        <t-input-number
+          v-model="localUpstreamData.checks.active.port"
+          :placeholder="t('components.upstreamForm.healthCheck.active.portPlaceholder')"
+          theme="normal"
+          :min="1"
+          :max="65535"
+          style="width: 100%"
+        />
+      </t-form-item>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.timeoutLabel')"
+            name="checks.active.timeout"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.timeout"
+              :placeholder="t('components.upstreamForm.healthCheck.active.timeoutPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.concurrencyLabel')"
+            name="checks.active.concurrency"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.concurrency"
+              :placeholder="t('components.upstreamForm.healthCheck.active.concurrencyPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-form-item
+        v-if="localUpstreamData.checks.active.type === 'https'"
+        :label="t('components.upstreamForm.healthCheck.active.httpsVerifyCertificateLabel')"
+        name="checks.active.https_verify_certificate"
+      >
+        <t-switch v-model="localUpstreamData.checks.active.https_verify_certificate" />
+      </t-form-item>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.healthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.intervalLabel')"
+            name="checks.active.healthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.successesLabel')"
+            name="checks.active.healthy.successes"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.successes']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.successes"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.successesPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesLabel')"
+            name="checks.active.healthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.healthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.unhealthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.intervalLabel')"
+            name="checks.active.unhealthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresLabel')"
+            name="checks.active.unhealthy.http_failures"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.http_failures']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.http_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            v-if="localUpstreamData.checks.active.type === 'tcp'"
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresLabel')"
+            name="checks.active.unhealthy.tcp_failures"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.tcp_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              :max="254"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesLabel')"
+            name="checks.active.unhealthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.unhealthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+    </div>
+  </t-form>
+</template>
+
+<script setup lang="ts">
+import { cloneDeep } from 'lodash';
+import { FormInstanceFunctions, MessagePlugin } from 'tdesign-vue-next';
+import { reactive, ref, watch } from 'vue';
+
+import NumberTagInput from '@/components/number-tag-input/index.vue';
+import { t } from '@/locales';
+
+import {
+  ACTIVE_HEALTH_CHECK_TYPES,
+  DISCOVERY_TYPE_OPTIONS,
+  HEALTH_CHECK_RULES,
+  LOAD_BALANCER_OPTIONS,
+  PROTOCOL_OPTIONS,
+} from './constants';
+
+interface UiNode {
+  host: string;
+  port: number | null;
+  weight: number;
+}
+
+// Define props for v-model
+// Use Record<string, any> to accept a generic key-value object
+const props = defineProps<{
+  modelValue: Record<string, any> | undefined | null;
+}>();
+
+// --- Internal State ---
+const formRef = ref<FormInstanceFunctions>();
+const isHealthCheckActive = ref(false); // Controls visibility and data structure of health checks
+const upstreamType = ref<'nodes' | 'discovery'>('nodes'); // Controls UI for nodes vs discovery
+
+// Internal representation for UI nodes using the local UiNode interface
+const uiNodes = ref<UiNode[]>([{ host: '', port: null, weight: 1 }]);
+
+const localUpstreamData = reactive<Record<string, any>>(getDefaultUpstreamData());
+
+// --- Helper Functions ---
+
+// Parses APISIX nodes object {"host:port": weight} into UI array [{host, port, weight}]
+// Returns the local UiNode type
+function parseNodesToUi(apiNodes: Record<string, number> | undefined): UiNode[] {
+  if (!apiNodes || typeof apiNodes !== 'object' || Object.keys(apiNodes).length === 0) {
+    return [{ host: '', port: null, weight: 1 }]; // Return default if empty or invalid
+  }
+  const parsed: UiNode[] = [];
+  for (const key in apiNodes) {
+    if (Object.prototype.hasOwnProperty.call(apiNodes, key)) {
+      const weight = apiNodes[key];
+      const parts = key.split(':');
+      const host = parts[0];
+      const port = parts.length > 1 ? parseInt(parts[1], 10) : null;
+
+      if (host && port !== null && !Number.isNaN(port) && typeof weight === 'number') {
+        parsed.push({ host, port, weight });
+      } else {
+        console.warn(`[UpstreamForm] Skipping invalid node entry: ${key}:${weight}`);
+        MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+      }
+    }
+  }
+  return parsed.length > 0 ? parsed : [{ host: '', port: null, weight: 1 }]; // Ensure at least one row if parsing resulted in empty
+}
+
+// Converts UI array [{host, port, weight}] back to APISIX object {"host:port": weight}
+// Accepts the local UiNode type
+function convertNodesToApi(nodesArray: UiNode[]): Record<string, number> | undefined {
+  const apiNodes: Record<string, number> = {};
+  let validNodes = 0;
+  nodesArray.forEach((node) => {
+    if (node.host && node.port && node.weight !== null && node.weight >= 0) {
+      apiNodes[`${node.host}:${node.port}`] = node.weight;
+      validNodes++;
+    }
+  });
+  return validNodes > 0 ? apiNodes : undefined;
+}
+
+// Gets default structure, ensuring nested objects exist for v-model bindings
+// Returns a plain object structure (Record<string, any>)
+function getDefaultUpstreamData(): Record<string, any> {
+  return {
+    type: 'roundrobin',
+    nodes: undefined, // Will be populated by conversion or default
+    // --- Discovery ---
+    discovery_type: undefined,
+    service_name: undefined,
+    pass_host: 'pass',
+    upstream_host: undefined,
+    // --- Retries ---
+    retries: undefined,
+    retry_timeout: undefined,
+    // --- Protocol & Timeouts ---
+    scheme: 'http',
+    timeout: { connect: 15, send: 15, read: 15 },
+    // --- Keepalive ---
+    keepalive_pool: { size: 320, idle_timeout: 60, requests: 1000 },
+    // --- Health Checks (Structure needed for v-model) ---
+    checks: {
+      active: {
+        type: 'http',
+        http_path: '/',
+        host: undefined,
+        port: undefined,
+        timeout: 1,
+        concurrency: 10,
+        https_verify_certificate: true,
+        // TODO(icebound): Add req_headers features.
+        req_headers: [],
+        healthy: {
+          interval: 1,
+          successes: 2,
+          http_statuses: [200, 302],
+        },
+        unhealthy: {
+          interval: 1,
+          http_failures: 5,
+          tcp_failures: 2,
+          timeouts: 3,
+          http_statuses: [429, 404, 500, 501, 502, 503, 504, 505],
+        },
+      },
+    },
+  };
+}
+
+const performReset = () => {
+  Object.assign(localUpstreamData, getDefaultUpstreamData());
+  uiNodes.value = parseNodesToUi(undefined);
+  upstreamType.value = 'nodes';
+  isHealthCheckActive.value = false;
+};
+
+// --- Watchers for Data Synchronization ---
+
+// Watch incoming modelValue to update local state and UI nodes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal === null || newVal === undefined) {
+      performReset();
+      return;
+    }
+
+    const incoming: Record<string, any> = newVal;
+    const defaults = getDefaultUpstreamData();
+
+    // Determine upstream type (nodes vs discovery)
+    if (incoming.discovery_type) {
+      upstreamType.value = 'discovery';
+    } else {
+      upstreamType.value = 'nodes';
+    }
+
+    // Merge top-level fields
+    Object.assign(localUpstreamData, {
+      ...defaults,
+      ...incoming,
+      // Ensure nested objects are properly merged/initialized
+      timeout: { ...defaults.timeout, ...(incoming.timeout || {}) },
+      keepalive_pool: { ...defaults.keepalive_pool, ...(incoming.keepalive_pool || {}) },
+      // Deep merge checks, ensuring structure exists
+      checks: deepMergeChecks(defaults.checks, incoming.checks),
+    });
+
+    // Convert API nodes format to UI format
+    if (upstreamType.value === 'nodes') {
+      uiNodes.value = parseNodesToUi(incoming.nodes as Record<string, number> | undefined);
+      localUpstreamData.nodes = incoming.nodes;
+    } else {
+      uiNodes.value = [{ host: '', port: null, weight: 1 }];
+      localUpstreamData.nodes = undefined;
+    }
+
+    isHealthCheckActive.value = Boolean(localUpstreamData.checks?.active);
+
+    if (isHealthCheckActive.value) {
+      if (localUpstreamData.checks.active.type === 'tcp') {
+        delete localUpstreamData.checks.active.http_path;
+        delete localUpstreamData.checks.active.host;
+      }
+      if (localUpstreamData.checks.active.type !== 'https') {
+        delete localUpstreamData.checks.active.https_verify_certificate;
+      }
+    }
+  },
+  { immediate: true, deep: false }, // Use deep: false initially, rely on specific watchers below for deep changes
+);
+
+// Helper for deep merging checks structure (accepts/returns 'any' for flexibility)
+function deepMergeChecks(defaults: any, incoming: any): any {
+  if (!incoming) return defaults;
+  const merged = { ...defaults };
+  if (incoming.active) {
+    merged.active = {
+      ...(defaults.active || {}),
+      ...incoming.active,
+      healthy: { ...(defaults.active?.healthy || {}), ...(incoming.active.healthy || {}) },
+      unhealthy: { ...(defaults.active?.unhealthy || {}), ...(incoming.active.unhealthy || {}) },
+    };
+  }
+  // TODO(icebound): Add merging for passive checks here.
+  // if (incoming.passive) { ... }
+  return merged;
+}
+
+// --- UI Interaction Methods ---
+
+const addNode = () => {
+  uiNodes.value.push({ host: '', port: null, weight: 1 });
+};
+
+const removeNode = (index: number) => {
+  if (uiNodes.value.length > 1) {
+    uiNodes.value.splice(index, 1);
+  }
+};
+
+// --- Expose Methods ---
+defineExpose({
+  validate: () => {
+    if (upstreamType.value === 'nodes') {
+      // Validate each node's host, port, and weight
+      for (const node of uiNodes.value) {
+        if (!node.host || !node.port || node.weight === null || node.weight < 0) {
+          MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+          return false;
+        }
+      }
+    }
+    return formRef.value?.validate();
+  },
+  getApiFormattedData: (): Record<string, any> => {
+    const result: Record<string, any> = cloneDeep(localUpstreamData);
+    if (upstreamType.value === 'nodes') {
+      delete result.discovery_type;
+      delete result.service_name;
+      result.nodes = convertNodesToApi(uiNodes.value);
+      if (!result.nodes || Object.keys(result.nodes).length === 0) {
+        delete result.nodes;
+      }
+    } else {
+      delete result.nodes;
+      if (!result.discovery_type) {
+        delete result.service_name;
+      }
+    }
+
+    if (result.pass_host !== 'rewrite') {
+      delete result.upstream_host;
+    }
+
+    if (!isHealthCheckActive.value || !result.checks?.active) {
+      if (result.checks) {
+        delete result.checks.active;
+        if (!result.checks.passive) {
+          delete result.checks;
+        }
+      }
+    } else {
+      const activeChecks = result.checks.active;
+      const { type } = activeChecks;
+
+      if (type === 'tcp') {
+        delete activeChecks.http_path;
+        delete activeChecks.host;
+        delete activeChecks.https_verify_certificate;
+        if (activeChecks.healthy) {
+          delete activeChecks.healthy.http_statuses;
+        }
+        if (activeChecks.unhealthy) {
+          delete activeChecks.unhealthy.http_statuses;
+          delete activeChecks.unhealthy.http_failures;
+        }
+      } else if (type !== 'https') {
+        delete activeChecks.https_verify_certificate;
+      }
+      if (activeChecks.http_path === '') delete activeChecks.http_path;
+      if (activeChecks.host === '') delete activeChecks.host;
+    }
+
+    Object.keys(result).forEach((key) => {
+      if (result[key] === undefined) {
+        delete result[key];
+      }
+    });
+
+    if (result.checks && Object.keys(result.checks).length === 0) {
+      delete result.checks;
+    }
+    return result;
+  },
+  resetForm: performReset,
+});
+</script>
+
+<style lang="less" scoped>
+@import './upstream-form.less';
+</style>
diff --git a/src/locales/lang/zh_CN/components/index.ts b/src/locales/lang/zh_CN/components/index.ts
index 8cec9f4..d08eed6 100644
--- a/src/locales/lang/zh_CN/components/index.ts
+++ b/src/locales/lang/zh_CN/components/index.ts
@@ -3,6 +3,7 @@ import components from './components';
 import configEditor from './plugin/config-editor';
 import configList from './plugin/config-list';
 import schemaInfo from './plugin/schema-info';
+import upstreamForm from './upstream/upstream-form';
 
 export default {
   components,
@@ -12,4 +13,5 @@ export default {
     configEditor,
     schemaInfo,
   },
+  upstreamForm,
 };
diff --git a/src/locales/lang/zh_CN/components/upstream/upstream-form.ts b/src/locales/lang/zh_CN/components/upstream/upstream-form.ts
new file mode 100644
index 0000000..527c8cd
--- /dev/null
+++ b/src/locales/lang/zh_CN/components/upstream/upstream-form.ts
@@ -0,0 +1,150 @@
+export default {
+  // 负载均衡
+  loadBalancer: {
+    label: '负载均衡算法',
+    placeholder: '请选择负载均衡算法',
+    roundRobin: '带权轮询',
+    consistentHash: '一致性哈希',
+    ewma: '指数加权平均(EWMA)',
+    leastConn: '最少连接数',
+  },
+
+  // 上游类型
+  upstreamType: {
+    label: '上游类型',
+    nodes: '节点列表',
+    discovery: '服务发现',
+  },
+
+  // 节点配置
+  nodes: {
+    label: '节点配置',
+    hostLabel: 'Host',
+    hostPlaceholder: '请输入域名或IP地址',
+    portLabel: '端口',
+    portPlaceholder: '请输入端口号',
+    weightLabel: '权重',
+    weightPlaceholder: '请输入权重',
+    addNode: '添加节点',
+    removeNode: '移除节点',
+  },
+
+  // 服务发现配置
+  discovery: {
+    typeLabel: '服务发现类型',
+    typePlaceholder: '请选择服务发现类型',
+    serviceNameLabel: '服务名称',
+    serviceNamePlaceholder: '请输入服务名称',
+    hostHeaderLabel: 'Host Header传递方式',
+    hostHeaderPlaceholder: '请选择Host Header传递方式',
+    hostHeaderPass: '透传',
+    hostHeaderNode: '使用节点',
+    hostHeaderRewrite: '重写',
+    upstreamHostLabel: '重写后的上游Host',
+    upstreamHostPlaceholder: '请输入重写后的上游Host',
+  },
+
+  // 重试配置
+  retries: {
+    label: '重试次数',
+    placeholder: '请输入重试次数',
+  },
+
+  retryTimeout: {
+    label: '重试超时',
+    placeholder: '请输入重试超时时间',
+  },
+
+  // 协议配置
+  protocol: {
+    label: '协议',
+    placeholder: '请选择协议',
+  },
+
+  // 连接超时配置
+  connectTimeout: {
+    label: '连接超时',
+    placeholder: '请输入连接超时时间',
+  },
+
+  sendTimeout: {
+    label: '发送超时',
+    placeholder: '请输入发送超时时间',
+  },
+
+  receiveTimeout: {
+    label: '接收超时',
+    placeholder: '请输入接收超时时间',
+  },
+
+  // 连接池配置
+  connectionPool: {
+    title: '连接池设置',
+    capacity: {
+      label: '连接池容量',
+      placeholder: '请输入连接池容量',
+    },
+    idleTimeout: {
+      label: '空闲超时',
+      placeholder: '请输入空闲超时时间',
+    },
+    maxRequests: {
+      label: '最大请求数',
+      placeholder: '请输入最大请求数',
+    },
+  },
+
+  // 健康检查配置
+  healthCheck: {
+    title: '健康检查',
+    active: {
+      enableLabel: '启用主动健康检查',
+      healthCheckType: '健康检查类型',
+      httpPathLabel: 'HTTP检查路径',
+      httpPathPlaceholder: '请输入HTTP检查路径',
+      httpPathRequired: '请填写HTTP检查路径',
+      hostLabel: 'Host Header',
+      hostPlaceholder: '请输入Host Header',
+      hostRequired: '请填写Host Header',
+      timeoutLabel: '检查超时时间',
+      timeoutPlaceholder: '请输入检查超时时间',
+      concurrencyLabel: '并发检查数',
+      concurrencyPlaceholder: '请输入并发检查数',
+      portLabel: '检查端口',
+      portPlaceholder: '请输入检查端口',
+      httpsVerifyCertificateLabel: '验证SSL证书',
+      healthy: {
+        title: '健康判定条件',
+        intervalLabel: '检查间隔',
+        intervalPlaceholder: '请输入健康检查间隔时间',
+        intervalRequired: '请填写健康检查间隔时间',
+        successesLabel: '成功次数',
+        successesPlaceholder: '请输入判定为健康所需的成功次数',
+        successesRequired: '请填写健康判定所需的成功次数',
+        httpStatusesLabel: 'HTTP状态码',
+        httpStatusesPlaceholder: '输入HTTP状态码并回车',
+      },
+      unhealthy: {
+        title: '不健康判定条件',
+        intervalLabel: '检查间隔',
+        intervalPlaceholder: '请输入不健康检查间隔时间',
+        intervalRequired: '请填写不健康检查间隔时间',
+        httpFailuresLabel: 'HTTP失败次数',
+        httpFailuresPlaceholder: '请输入判定为不健康所需的HTTP失败次数',
+        httpFailuresRequired: '请填写不健康判定所需的HTTP失败次数',
+        tcpFailuresLabel: 'TCP失败次数',
+        tcpFailuresPlaceholder: '请输入判定为不健康所需的TCP失败次数',
+        timeoutsLabel: '超时次数',
+        timeoutsPlaceholder: '请输入判定为不健康所需的超时次数',
+        httpStatusesLabel: 'HTTP状态码',
+        httpStatusesPlaceholder: '输入HTTP状态码并回车',
+      },
+    },
+  },
+
+  // 警告信息
+  warnings: {
+    invalidNodeFormat: '节点格式无效',
+    invalidHttpStatusCode: 'HTTP状态码必须是200-599之间的数字',
+  },
+};
diff --git a/src/router/index.ts b/src/router/index.ts
index 224c60a..6ba50b9 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -19,7 +19,26 @@ const defaultRouterList: Array<RouteRecordRaw> = [
 // 存放固定路由
 export const fixedRouterList: Array<RouteRecordRaw> = mapModuleRouterList(fixedModules);
 
-export const allRoutes = [...fixedRouterList, ...defaultRouterList];
+// Harness 路由
+const harnessRoute: RouteRecordRaw = {
+  path: '/dev/upstream-form-harness',
+  name: 'UpstreamFormHarness',
+  component: () => import('@/components/upstream/upstream-form-harness.vue'),
+  meta: {
+    title: 'UpstreamForm Harness',
+    requiresAuth: false,
+  },
+};
+
+const devRoutes: Array<RouteRecordRaw> = [];
+
+// 仅在开发模式下添加 Harness 路由
+if (import.meta.env.DEV) {
+  devRoutes.push(harnessRoute);
+  console.log('[Router] Added development route:', harnessRoute.path); // 方便调试时确认
+}
+
+export const allRoutes = [...fixedRouterList, ...defaultRouterList, ...devRoutes];
 
 // 固定路由模块转换为路由
 export function mapModuleRouterList(modules: Record<string, unknown>): Array<RouteRecordRaw> {
@@ -58,6 +77,11 @@ export const getRoutesExpanded = () => {
 
 export const getActive = (maxLevel = 3): string => {
   // 非组件内调用必须通过Router实例获取当前路由
+  // 确保 router 实例在使用前已创建
+  if (!router) {
+    console.warn('[Router] getActive called before router instance is created.');
+    return '';
+  }
   const route = router.currentRoute.value;
 
   if (!route.path) {


Reviews:
user: login: Lensual
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T05:23:26Z
user: login: Lensual
body: good job
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-03T17:53:28Z
user: login: iceboundx
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-04T08:15:35Z
user: login: iceboundx
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-04T08:16:07Z

Review comments:
diff_hunk: @@ -0,0 +1,150 @@
+export default {
+  // 负载均衡
+  loadBalancer: {
+    label: '负载均衡算法',
+    placeholder: '请选择负载均衡算法',
+    roundRobin: '带权轮询',
+    consistentHash: '一致性哈希',
+    ewma: '指数加权平均(EWMA)',
+    leastConn: '最少连接数',
+  },
+
+  // 上游类型
+  upstreamType: {
+    label: '上游类型',
+    nodes: '节点列表',
+    discovery: '服务发现',
+  },
+
+  // 节点配置
+  nodes: {
+    label: '节点配置',
+    hostLabel: '主机',
+    hostPlaceholder: '请输入主机名或IP地址',
+    portLabel: '端口',
+    portPlaceholder: '请输入端口号',
+    weightLabel: '权重',
+    weightPlaceholder: '请输入权重',
+    addNode: '添加节点',
+    removeNode: '移除节点',
+  },
+
+  // 服务发现配置
+  discovery: {
+    typeLabel: '服务发现类型',
+    typePlaceholder: '请选择服务发现类型',
+    serviceNameLabel: '服务名称',
+    serviceNamePlaceholder: '请输入服务名称',
+    hostHeaderLabel: '主机头传递方式',
+    hostHeaderPlaceholder: '请选择主机头传递方式',
path: src/locales/lang/zh_CN/components/upstream/upstream-form.ts
user: login: Lensual
body: 统一一下主机相关的词汇
created_at: 2025-05-01T22:13:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,150 @@
+export default {
+  // 负载均衡
+  loadBalancer: {
+    label: '负载均衡算法',
+    placeholder: '请选择负载均衡算法',
+    roundRobin: '带权轮询',
+    consistentHash: '一致性哈希',
+    ewma: '指数加权平均(EWMA)',
+    leastConn: '最少连接数',
+  },
+
+  // 上游类型
+  upstreamType: {
+    label: '上游类型',
+    nodes: '节点列表',
+    discovery: '服务发现',
+  },
+
+  // 节点配置
+  nodes: {
+    label: '节点配置',
+    hostLabel: '主机',
+    hostPlaceholder: '请输入主机名或IP地址',
path: src/locales/lang/zh_CN/components/upstream/upstream-form.ts
user: login: Lensual
body: 统一一下主机相关的词汇
created_at: 2025-05-01T22:18:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,150 @@
+export default {
+  // 负载均衡
+  loadBalancer: {
+    label: '负载均衡算法',
+    placeholder: '请选择负载均衡算法',
+    roundRobin: '带权轮询',
+    consistentHash: '一致性哈希',
+    ewma: '指数加权平均(EWMA)',
+    leastConn: '最少连接数',
+  },
+
+  // 上游类型
+  upstreamType: {
+    label: '上游类型',
+    nodes: '节点列表',
+    discovery: '服务发现',
+  },
+
+  // 节点配置
+  nodes: {
+    label: '节点配置',
+    hostLabel: '主机',
+    hostPlaceholder: '请输入主机名或IP地址',
+    portLabel: '端口',
+    portPlaceholder: '请输入端口号',
+    weightLabel: '权重',
+    weightPlaceholder: '请输入权重',
+    addNode: '添加节点',
+    removeNode: '移除节点',
+  },
+
+  // 服务发现配置
+  discovery: {
+    typeLabel: '服务发现类型',
+    typePlaceholder: '请选择服务发现类型',
+    serviceNameLabel: '服务名称',
+    serviceNamePlaceholder: '请输入服务名称',
+    hostHeaderLabel: '主机头传递方式',
+    hostHeaderPlaceholder: '请选择主机头传递方式',
+    hostHeaderPass: '透传',
+    hostHeaderNode: '使用节点',
+    hostHeaderRewrite: '重写',
+    upstreamHostLabel: '上游主机名',
+    upstreamHostPlaceholder: '请输入上游主机名',
+  },
+
+  // 重试配置
+  retries: {
+    label: '重试次数',
+    placeholder: '请输入重试次数',
+  },
+
+  retryTimeout: {
+    label: '重试超时',
+    placeholder: '请输入重试超时时间',
+  },
+
+  // 协议配置
+  protocol: {
+    label: '协议',
+    placeholder: '请选择协议',
+  },
+
+  // 连接超时配置
+  connectTimeout: {
+    label: '连接超时',
+    placeholder: '请输入连接超时时间',
+  },
+
+  sendTimeout: {
+    label: '发送超时',
+    placeholder: '请输入发送超时时间',
+  },
+
+  receiveTimeout: {
+    label: '接收超时',
+    placeholder: '请输入接收超时时间',
+  },
+
+  // 连接池配置
+  connectionPool: {
+    title: '连接池设置',
+    capacity: {
+      label: '连接池容量',
+      placeholder: '请输入连接池容量',
+    },
+    idleTimeout: {
+      label: '空闲超时',
+      placeholder: '请输入空闲超时时间',
+    },
+    maxRequests: {
+      label: '最大请求数',
+      placeholder: '请输入最大请求数',
+    },
+  },
+
+  // 健康检查配置
+  healthCheck: {
+    title: '健康检查',
+    active: {
+      enableLabel: '启用主动健康检查',
+      healthCheckType: '健康检查类型',
+      httpPathLabel: 'HTTP检查路径',
+      httpPathPlaceholder: '请输入HTTP检查路径',
+      httpPathRequired: '请填写HTTP检查路径',
+      hostLabel: '主机名',
+      hostPlaceholder: '请输入主机名',
+      hostRequired: '请填写主机名',
path: src/locales/lang/zh_CN/components/upstream/upstream-form.ts
user: login: Lensual
body: 统一一下主机相关的词汇
created_at: 2025-05-01T22:19:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-select
+          v-model="localUpstreamData.pass_host"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-option value="pass" :label="t('components.upstreamForm.discovery.hostHeaderPass')" />
+          <t-option value="node" :label="t('components.upstreamForm.discovery.hostHeaderNode')" />
+          <t-option value="rewrite" :label="t('components.upstreamForm.discovery.hostHeaderRewrite')" />
+        </t-select>
+      </t-form-item>
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: 主机头传递方式

涉及到下文表单变更，建议使用按钮Radio组件（t-radio-group、t-radio-button）

后续可以考虑为选项增加tips
created_at: 2025-05-01T22:23:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: 建议使用按钮Radio组件（t-radio-group、t-radio-button）
created_at: 2025-05-01T22:31:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+<script setup lang="ts">
+import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next'; // 假设可以导入类型
path: src/components/number-tag-input/index.vue
user: login: Lensual
body: ?
created_at: 2025-05-01T23:20:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,50 @@
+import { t } from '@/locales';
+
+export const LOAD_BALANCER_OPTIONS = [
+  { label: t('components.upstreamForm.loadBalancer.roundRobin'), value: 'roundrobin' },
+  { label: t('components.upstreamForm.loadBalancer.consistentHash'), value: 'chash' },
+  { label: t('components.upstreamForm.loadBalancer.ewma'), value: 'ewma' },
+  { label: t('components.upstreamForm.loadBalancer.leastConn'), value: 'least_conn' },
+];
+
+export const DISCOVERY_TYPE_OPTIONS = [
+  { label: 'DNS', value: 'dns' },
+  { label: 'Consul', value: 'consul' },
+  { label: 'Consul KV', value: 'consul_kv' },
+  { label: 'Kubernetes', value: 'kubernetes' },
+  { label: 'Nacos', value: 'nacos' },
+  { label: 'Eureka', value: 'eureka' },
+];
+
+export const PROTOCOL_OPTIONS = [
+  { label: 'HTTP', value: 'http' },
+  { label: 'HTTPS', value: 'https' },
+  { label: 'gRPC', value: 'grpc' },
+  { label: 'gRPCS', value: 'grpcs' },
+];
+
+export const ACTIVE_HEALTH_CHECK_TYPES = [
+  { label: 'HTTP', value: 'http' },
+  { label: 'HTTPS', value: 'https' },
+  { label: 'tcp', value: 'tcp' },
+];
+
+interface ValidationRule {
+  required?: boolean;
+  message: string;
+}
+
+export const HEALTH_CHECK_RULES: Record<string, ValidationRule[]> = {
+  'checks.active.healthy.interval': [
path: src/components/upstream/constants.ts
user: login: Lensual
body: ```
import { FormRule } from 'tdesign-vue-next';
export const HEALTH_CHECK_RULES: Record<string, FormRule[]> = {
...
```
created_at: 2025-05-01T23:24:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-select
+          v-model="localUpstreamData.pass_host"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-option value="pass" :label="t('components.upstreamForm.discovery.hostHeaderPass')" />
+          <t-option value="node" :label="t('components.upstreamForm.discovery.hostHeaderNode')" />
+          <t-option value="rewrite" :label="t('components.upstreamForm.discovery.hostHeaderRewrite')" />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        v-if="localUpstreamData.pass_host === 'rewrite'"
+        :label="t('components.upstreamForm.discovery.upstreamHostLabel')"
+        name="upstream_host"
+      >
+        <t-input
+          v-model="localUpstreamData.upstream_host"
+          :placeholder="t('components.upstreamForm.discovery.upstreamHostPlaceholder')"
+        />
+      </t-form-item>
+    </div>
+
+    <!-- 3. Retries and Retry Timeout -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retries.label')" name="retries">
+          <t-input-number
+            v-model="localUpstreamData.retries"
+            :placeholder="t('components.upstreamForm.retries.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retryTimeout.label')" name="retry_timeout">
+          <t-input-number
+            v-model="localUpstreamData.retry_timeout"
+            :placeholder="t('components.upstreamForm.retryTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 4. Protocol -->
+    <t-form-item :label="t('components.upstreamForm.protocol.label')" name="scheme" required-mark>
+      <t-select v-model="localUpstreamData.scheme" :placeholder="t('components.upstreamForm.protocol.placeholder')">
+        <t-option v-for="item in PROTOCOL_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 5. Timeout Settings -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectTimeout.label')" name="timeout.connect">
+          <t-input-number
+            v-model="localUpstreamData.timeout.connect"
+            :placeholder="t('components.upstreamForm.connectTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.sendTimeout.label')" name="timeout.send">
+          <t-input-number
+            v-model="localUpstreamData.timeout.send"
+            :placeholder="t('components.upstreamForm.sendTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.receiveTimeout.label')" name="timeout.read">
+          <t-input-number
+            v-model="localUpstreamData.timeout.read"
+            :placeholder="t('components.upstreamForm.receiveTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 6. Connection Pool -->
+    <t-divider>{{ t('components.upstreamForm.connectionPool.title') }}</t-divider>
+    <t-row :gutter="[16, 16]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectionPool.capacity.label')" name="keepalive_pool.size">
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.size"
+            :placeholder="t('components.upstreamForm.connectionPool.capacity.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.idleTimeout.label')"
+          name="keepalive_pool.idle_timeout"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.idle_timeout"
+            :placeholder="t('components.upstreamForm.connectionPool.idleTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.maxRequests.label')"
+          name="keepalive_pool.requests"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.requests"
+            :placeholder="t('components.upstreamForm.connectionPool.maxRequests.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 7. Health Checks -->
+    <t-divider>{{ t('components.upstreamForm.healthCheck.title') }}</t-divider>
+    <t-form-item :label="t('components.upstreamForm.healthCheck.active.enableLabel')" name="healthCheckActiveEnabled">
+      <t-switch v-model="isHealthCheckActive" />
+    </t-form-item>
+
+    <div v-if="isHealthCheckActive">
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.healthCheckType')"
+        name="checks.active.type"
+        required-mark
+      >
+        <t-select v-model="localUpstreamData.checks.active.type">
+          <t-option
+            v-for="item in ACTIVE_HEALTH_CHECK_TYPES"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+          />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.httpPathLabel')"
+        name="checks.active.http_path"
+        required-mark
+      >
+        <t-input
+          v-model="localUpstreamData.checks.active.http_path"
+          :placeholder="t('components.upstreamForm.healthCheck.active.httpPathPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.hostLabel')" name="checks.active.host">
+        <t-input
+          v-model="localUpstreamData.checks.active.host"
+          :placeholder="t('components.upstreamForm.healthCheck.active.hostPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.portLabel')" name="checks.active.port">
+        <t-input-number
+          v-model="localUpstreamData.checks.active.port"
+          :placeholder="t('components.upstreamForm.healthCheck.active.portPlaceholder')"
+          theme="normal"
+          :min="1"
+          :max="65535"
+          style="width: 100%"
+        />
+      </t-form-item>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.timeoutLabel')"
+            name="checks.active.timeout"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.timeout"
+              :placeholder="t('components.upstreamForm.healthCheck.active.timeoutPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.concurrencyLabel')"
+            name="checks.active.concurrency"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.concurrency"
+              :placeholder="t('components.upstreamForm.healthCheck.active.concurrencyPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-form-item
+        v-if="localUpstreamData.checks.active.type === 'https'"
+        :label="t('components.upstreamForm.healthCheck.active.httpsVerifyCertificateLabel')"
+        name="checks.active.https_verify_certificate"
+      >
+        <t-switch v-model="localUpstreamData.checks.active.https_verify_certificate" />
+      </t-form-item>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.healthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.intervalLabel')"
+            name="checks.active.healthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.successesLabel')"
+            name="checks.active.healthy.successes"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.successes']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.successes"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.successesPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesLabel')"
+            name="checks.active.healthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.healthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.unhealthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.intervalLabel')"
+            name="checks.active.unhealthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresLabel')"
+            name="checks.active.unhealthy.http_failures"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.http_failures']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.http_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            v-if="localUpstreamData.checks.active.type === 'tcp'"
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresLabel')"
+            name="checks.active.unhealthy.tcp_failures"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.tcp_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              :max="254"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesLabel')"
+            name="checks.active.unhealthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.unhealthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+    </div>
+  </t-form>
+</template>
+
+<script setup lang="ts">
+import { FormInstanceFunctions, MessagePlugin } from 'tdesign-vue-next';
+import { reactive, ref, watch } from 'vue';
+
+import NumberTagInput from '@/components/number-tag-input/index.vue';
+import { t } from '@/locales';
+
+import {
+  ACTIVE_HEALTH_CHECK_TYPES,
+  DISCOVERY_TYPE_OPTIONS,
+  HEALTH_CHECK_RULES,
+  LOAD_BALANCER_OPTIONS,
+  PROTOCOL_OPTIONS,
+} from './constants';
+
+interface UiNode {
+  host: string;
+  port: number | null;
+  weight: number;
+}
+
+// Define props for v-model
+// Use Record<string, any> to accept a generic key-value object
+const props = defineProps<{
+  modelValue: Record<string, any> | undefined | null;
+}>();
+
+// --- Internal State ---
+const formRef = ref<FormInstanceFunctions>();
+const isHealthCheckActive = ref(false); // Controls visibility and data structure of health checks
+const upstreamType = ref<'nodes' | 'discovery'>('nodes'); // Controls UI for nodes vs discovery
+
+// Internal representation for UI nodes using the local UiNode interface
+const uiNodes = ref<UiNode[]>([{ host: '', port: null, weight: 1 }]);
+
+const localUpstreamData = reactive<Record<string, any>>(getDefaultUpstreamData());
+
+// --- Helper Functions ---
+
+// Parses APISIX nodes object {"host:port": weight} into UI array [{host, port, weight}]
+// Returns the local UiNode type
+function parseNodesToUi(apiNodes: Record<string, number> | undefined): UiNode[] {
+  if (!apiNodes || typeof apiNodes !== 'object' || Object.keys(apiNodes).length === 0) {
+    return [{ host: '', port: null, weight: 1 }]; // Return default if empty or invalid
+  }
+  const parsed: UiNode[] = [];
+  for (const key in apiNodes) {
+    if (Object.prototype.hasOwnProperty.call(apiNodes, key)) {
+      const weight = apiNodes[key];
+      const parts = key.split(':');
+      const host = parts[0];
+      const port = parts.length > 1 ? parseInt(parts[1], 10) : null;
+
+      if (host && port !== null && !Number.isNaN(port) && typeof weight === 'number') {
+        parsed.push({ host, port, weight });
+      } else {
+        console.warn(`[UpstreamForm] Skipping invalid node entry: ${key}:${weight}`);
+        MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+      }
+    }
+  }
+  return parsed.length > 0 ? parsed : [{ host: '', port: null, weight: 1 }]; // Ensure at least one row if parsing resulted in empty
+}
+
+// Converts UI array [{host, port, weight}] back to APISIX object {"host:port": weight}
+// Accepts the local UiNode type
+function convertNodesToApi(nodesArray: UiNode[]): Record<string, number> | undefined {
+  const apiNodes: Record<string, number> = {};
+  let validNodes = 0;
+  nodesArray.forEach((node) => {
+    if (node.host && node.port && node.weight !== null && node.weight >= 0) {
+      apiNodes[`${node.host}:${node.port}`] = node.weight;
+      validNodes++;
+    }
+  });
+  // Return undefined if no valid nodes to avoid sending empty {}
+  return validNodes > 0 ? apiNodes : undefined;
+}
+
+// Gets default structure, ensuring nested objects exist for v-model bindings
+// Returns a plain object structure (Record<string, any>)
+function getDefaultUpstreamData(): Record<string, any> {
+  return {
+    type: 'roundrobin',
+    nodes: undefined, // Will be populated by conversion or default
+    // --- Discovery ---
+    discovery_type: undefined,
+    service_name: undefined,
+    pass_host: 'pass',
+    upstream_host: undefined,
+    // --- Retries ---
+    retries: undefined,
+    retry_timeout: undefined,
+    // --- Protocol & Timeouts ---
+    scheme: 'http',
+    timeout: { connect: 15, send: 15, read: 15 },
+    // --- Keepalive ---
+    keepalive_pool: { size: 320, idle_timeout: 60, requests: 1000 },
+    // --- Health Checks (Structure needed for v-model) ---
+    checks: {
+      active: {
+        type: 'http',
+        http_path: '/',
+        host: undefined,
+        port: undefined,
+        timeout: 1,
+        concurrency: 10,
+        https_verify_certificate: true,
+        req_headers: [],
+        healthy: {
+          interval: 1,
+          successes: 2,
+          http_statuses: [200, 302],
+        },
+        unhealthy: {
+          interval: 1,
+          http_failures: 5,
+          tcp_failures: 2,
+          timeouts: 3,
+          http_statuses: [429, 404, 500, 501, 502, 503, 504, 505],
+        },
+      },
+    },
+  };
+}
+
+const performReset = () => {
+  Object.assign(localUpstreamData, getDefaultUpstreamData());
+  uiNodes.value = parseNodesToUi(undefined);
+  upstreamType.value = 'nodes';
+  isHealthCheckActive.value = false;
+};
+
+// --- Watchers for Data Synchronization ---
+
+// Watch incoming modelValue to update local state and UI nodes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal === null || newVal === undefined) {
+      performReset();
+      return;
+    }
+
+    const incoming: Record<string, any> = newVal;
+    const defaults = getDefaultUpstreamData();
+
+    // Determine upstream type (nodes vs discovery)
+    if (incoming.discovery_type) {
+      upstreamType.value = 'discovery';
+    } else {
+      upstreamType.value = 'nodes';
+    }
+
+    // Merge top-level fields
+    Object.assign(localUpstreamData, {
+      ...defaults,
+      ...incoming,
+      // Ensure nested objects are properly merged/initialized
+      timeout: { ...defaults.timeout, ...(incoming.timeout || {}) },
+      keepalive_pool: { ...defaults.keepalive_pool, ...(incoming.keepalive_pool || {}) },
+      // Deep merge checks, ensuring structure exists
+      checks: deepMergeChecks(defaults.checks, incoming.checks),
+    });
+
+    // Convert API nodes format to UI format
+    if (upstreamType.value === 'nodes') {
+      uiNodes.value = parseNodesToUi(incoming.nodes as Record<string, number> | undefined);
+      localUpstreamData.nodes = incoming.nodes;
+    } else {
+      uiNodes.value = [{ host: '', port: null, weight: 1 }];
+      localUpstreamData.nodes = undefined;
+    }
+
+    isHealthCheckActive.value = !!localUpstreamData.checks?.active;
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: 这里类型转换用语法糖不合适
created_at: 2025-05-01T23:32:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-select
+          v-model="localUpstreamData.pass_host"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-option value="pass" :label="t('components.upstreamForm.discovery.hostHeaderPass')" />
+          <t-option value="node" :label="t('components.upstreamForm.discovery.hostHeaderNode')" />
+          <t-option value="rewrite" :label="t('components.upstreamForm.discovery.hostHeaderRewrite')" />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        v-if="localUpstreamData.pass_host === 'rewrite'"
+        :label="t('components.upstreamForm.discovery.upstreamHostLabel')"
+        name="upstream_host"
+      >
+        <t-input
+          v-model="localUpstreamData.upstream_host"
+          :placeholder="t('components.upstreamForm.discovery.upstreamHostPlaceholder')"
+        />
+      </t-form-item>
+    </div>
+
+    <!-- 3. Retries and Retry Timeout -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retries.label')" name="retries">
+          <t-input-number
+            v-model="localUpstreamData.retries"
+            :placeholder="t('components.upstreamForm.retries.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retryTimeout.label')" name="retry_timeout">
+          <t-input-number
+            v-model="localUpstreamData.retry_timeout"
+            :placeholder="t('components.upstreamForm.retryTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 4. Protocol -->
+    <t-form-item :label="t('components.upstreamForm.protocol.label')" name="scheme" required-mark>
+      <t-select v-model="localUpstreamData.scheme" :placeholder="t('components.upstreamForm.protocol.placeholder')">
+        <t-option v-for="item in PROTOCOL_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 5. Timeout Settings -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectTimeout.label')" name="timeout.connect">
+          <t-input-number
+            v-model="localUpstreamData.timeout.connect"
+            :placeholder="t('components.upstreamForm.connectTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.sendTimeout.label')" name="timeout.send">
+          <t-input-number
+            v-model="localUpstreamData.timeout.send"
+            :placeholder="t('components.upstreamForm.sendTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.receiveTimeout.label')" name="timeout.read">
+          <t-input-number
+            v-model="localUpstreamData.timeout.read"
+            :placeholder="t('components.upstreamForm.receiveTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 6. Connection Pool -->
+    <t-divider>{{ t('components.upstreamForm.connectionPool.title') }}</t-divider>
+    <t-row :gutter="[16, 16]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectionPool.capacity.label')" name="keepalive_pool.size">
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.size"
+            :placeholder="t('components.upstreamForm.connectionPool.capacity.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.idleTimeout.label')"
+          name="keepalive_pool.idle_timeout"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.idle_timeout"
+            :placeholder="t('components.upstreamForm.connectionPool.idleTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.maxRequests.label')"
+          name="keepalive_pool.requests"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.requests"
+            :placeholder="t('components.upstreamForm.connectionPool.maxRequests.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 7. Health Checks -->
+    <t-divider>{{ t('components.upstreamForm.healthCheck.title') }}</t-divider>
+    <t-form-item :label="t('components.upstreamForm.healthCheck.active.enableLabel')" name="healthCheckActiveEnabled">
+      <t-switch v-model="isHealthCheckActive" />
+    </t-form-item>
+
+    <div v-if="isHealthCheckActive">
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.healthCheckType')"
+        name="checks.active.type"
+        required-mark
+      >
+        <t-select v-model="localUpstreamData.checks.active.type">
+          <t-option
+            v-for="item in ACTIVE_HEALTH_CHECK_TYPES"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+          />
+        </t-select>
+      </t-form-item>
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: 建议使用按钮Radio组件（t-radio-group、t-radio-button）
created_at: 2025-05-01T23:35:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-select
+          v-model="localUpstreamData.pass_host"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-option value="pass" :label="t('components.upstreamForm.discovery.hostHeaderPass')" />
+          <t-option value="node" :label="t('components.upstreamForm.discovery.hostHeaderNode')" />
+          <t-option value="rewrite" :label="t('components.upstreamForm.discovery.hostHeaderRewrite')" />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        v-if="localUpstreamData.pass_host === 'rewrite'"
+        :label="t('components.upstreamForm.discovery.upstreamHostLabel')"
+        name="upstream_host"
+      >
+        <t-input
+          v-model="localUpstreamData.upstream_host"
+          :placeholder="t('components.upstreamForm.discovery.upstreamHostPlaceholder')"
+        />
+      </t-form-item>
+    </div>
+
+    <!-- 3. Retries and Retry Timeout -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retries.label')" name="retries">
+          <t-input-number
+            v-model="localUpstreamData.retries"
+            :placeholder="t('components.upstreamForm.retries.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retryTimeout.label')" name="retry_timeout">
+          <t-input-number
+            v-model="localUpstreamData.retry_timeout"
+            :placeholder="t('components.upstreamForm.retryTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 4. Protocol -->
+    <t-form-item :label="t('components.upstreamForm.protocol.label')" name="scheme" required-mark>
+      <t-select v-model="localUpstreamData.scheme" :placeholder="t('components.upstreamForm.protocol.placeholder')">
+        <t-option v-for="item in PROTOCOL_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 5. Timeout Settings -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectTimeout.label')" name="timeout.connect">
+          <t-input-number
+            v-model="localUpstreamData.timeout.connect"
+            :placeholder="t('components.upstreamForm.connectTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.sendTimeout.label')" name="timeout.send">
+          <t-input-number
+            v-model="localUpstreamData.timeout.send"
+            :placeholder="t('components.upstreamForm.sendTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.receiveTimeout.label')" name="timeout.read">
+          <t-input-number
+            v-model="localUpstreamData.timeout.read"
+            :placeholder="t('components.upstreamForm.receiveTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 6. Connection Pool -->
+    <t-divider>{{ t('components.upstreamForm.connectionPool.title') }}</t-divider>
+    <t-row :gutter="[16, 16]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectionPool.capacity.label')" name="keepalive_pool.size">
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.size"
+            :placeholder="t('components.upstreamForm.connectionPool.capacity.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.idleTimeout.label')"
+          name="keepalive_pool.idle_timeout"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.idle_timeout"
+            :placeholder="t('components.upstreamForm.connectionPool.idleTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.maxRequests.label')"
+          name="keepalive_pool.requests"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.requests"
+            :placeholder="t('components.upstreamForm.connectionPool.maxRequests.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 7. Health Checks -->
+    <t-divider>{{ t('components.upstreamForm.healthCheck.title') }}</t-divider>
+    <t-form-item :label="t('components.upstreamForm.healthCheck.active.enableLabel')" name="healthCheckActiveEnabled">
+      <t-switch v-model="isHealthCheckActive" />
+    </t-form-item>
+
+    <div v-if="isHealthCheckActive">
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.healthCheckType')"
+        name="checks.active.type"
+        required-mark
+      >
+        <t-select v-model="localUpstreamData.checks.active.type">
+          <t-option
+            v-for="item in ACTIVE_HEALTH_CHECK_TYPES"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+          />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.httpPathLabel')"
+        name="checks.active.http_path"
+        required-mark
+      >
+        <t-input
+          v-model="localUpstreamData.checks.active.http_path"
+          :placeholder="t('components.upstreamForm.healthCheck.active.httpPathPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.hostLabel')" name="checks.active.host">
+        <t-input
+          v-model="localUpstreamData.checks.active.host"
+          :placeholder="t('components.upstreamForm.healthCheck.active.hostPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.portLabel')" name="checks.active.port">
+        <t-input-number
+          v-model="localUpstreamData.checks.active.port"
+          :placeholder="t('components.upstreamForm.healthCheck.active.portPlaceholder')"
+          theme="normal"
+          :min="1"
+          :max="65535"
+          style="width: 100%"
+        />
+      </t-form-item>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.timeoutLabel')"
+            name="checks.active.timeout"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.timeout"
+              :placeholder="t('components.upstreamForm.healthCheck.active.timeoutPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.concurrencyLabel')"
+            name="checks.active.concurrency"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.concurrency"
+              :placeholder="t('components.upstreamForm.healthCheck.active.concurrencyPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-form-item
+        v-if="localUpstreamData.checks.active.type === 'https'"
+        :label="t('components.upstreamForm.healthCheck.active.httpsVerifyCertificateLabel')"
+        name="checks.active.https_verify_certificate"
+      >
+        <t-switch v-model="localUpstreamData.checks.active.https_verify_certificate" />
+      </t-form-item>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.healthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.intervalLabel')"
+            name="checks.active.healthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.successesLabel')"
+            name="checks.active.healthy.successes"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.successes']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.successes"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.successesPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesLabel')"
+            name="checks.active.healthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.healthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.unhealthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.intervalLabel')"
+            name="checks.active.unhealthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresLabel')"
+            name="checks.active.unhealthy.http_failures"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.http_failures']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.http_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            v-if="localUpstreamData.checks.active.type === 'tcp'"
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresLabel')"
+            name="checks.active.unhealthy.tcp_failures"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.tcp_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              :max="254"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesLabel')"
+            name="checks.active.unhealthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.unhealthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+    </div>
+  </t-form>
+</template>
+
+<script setup lang="ts">
+import { FormInstanceFunctions, MessagePlugin } from 'tdesign-vue-next';
+import { reactive, ref, watch } from 'vue';
+
+import NumberTagInput from '@/components/number-tag-input/index.vue';
+import { t } from '@/locales';
+
+import {
+  ACTIVE_HEALTH_CHECK_TYPES,
+  DISCOVERY_TYPE_OPTIONS,
+  HEALTH_CHECK_RULES,
+  LOAD_BALANCER_OPTIONS,
+  PROTOCOL_OPTIONS,
+} from './constants';
+
+interface UiNode {
+  host: string;
+  port: number | null;
+  weight: number;
+}
+
+// Define props for v-model
+// Use Record<string, any> to accept a generic key-value object
+const props = defineProps<{
+  modelValue: Record<string, any> | undefined | null;
+}>();
+
+// --- Internal State ---
+const formRef = ref<FormInstanceFunctions>();
+const isHealthCheckActive = ref(false); // Controls visibility and data structure of health checks
+const upstreamType = ref<'nodes' | 'discovery'>('nodes'); // Controls UI for nodes vs discovery
+
+// Internal representation for UI nodes using the local UiNode interface
+const uiNodes = ref<UiNode[]>([{ host: '', port: null, weight: 1 }]);
+
+const localUpstreamData = reactive<Record<string, any>>(getDefaultUpstreamData());
+
+// --- Helper Functions ---
+
+// Parses APISIX nodes object {"host:port": weight} into UI array [{host, port, weight}]
+// Returns the local UiNode type
+function parseNodesToUi(apiNodes: Record<string, number> | undefined): UiNode[] {
+  if (!apiNodes || typeof apiNodes !== 'object' || Object.keys(apiNodes).length === 0) {
+    return [{ host: '', port: null, weight: 1 }]; // Return default if empty or invalid
+  }
+  const parsed: UiNode[] = [];
+  for (const key in apiNodes) {
+    if (Object.prototype.hasOwnProperty.call(apiNodes, key)) {
+      const weight = apiNodes[key];
+      const parts = key.split(':');
+      const host = parts[0];
+      const port = parts.length > 1 ? parseInt(parts[1], 10) : null;
+
+      if (host && port !== null && !Number.isNaN(port) && typeof weight === 'number') {
+        parsed.push({ host, port, weight });
+      } else {
+        console.warn(`[UpstreamForm] Skipping invalid node entry: ${key}:${weight}`);
+        MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+      }
+    }
+  }
+  return parsed.length > 0 ? parsed : [{ host: '', port: null, weight: 1 }]; // Ensure at least one row if parsing resulted in empty
+}
+
+// Converts UI array [{host, port, weight}] back to APISIX object {"host:port": weight}
+// Accepts the local UiNode type
+function convertNodesToApi(nodesArray: UiNode[]): Record<string, number> | undefined {
+  const apiNodes: Record<string, number> = {};
+  let validNodes = 0;
+  nodesArray.forEach((node) => {
+    if (node.host && node.port && node.weight !== null && node.weight >= 0) {
+      apiNodes[`${node.host}:${node.port}`] = node.weight;
+      validNodes++;
+    }
+  });
+  // Return undefined if no valid nodes to avoid sending empty {}
+  return validNodes > 0 ? apiNodes : undefined;
+}
+
+// Gets default structure, ensuring nested objects exist for v-model bindings
+// Returns a plain object structure (Record<string, any>)
+function getDefaultUpstreamData(): Record<string, any> {
+  return {
+    type: 'roundrobin',
+    nodes: undefined, // Will be populated by conversion or default
+    // --- Discovery ---
+    discovery_type: undefined,
+    service_name: undefined,
+    pass_host: 'pass',
+    upstream_host: undefined,
+    // --- Retries ---
+    retries: undefined,
+    retry_timeout: undefined,
+    // --- Protocol & Timeouts ---
+    scheme: 'http',
+    timeout: { connect: 15, send: 15, read: 15 },
+    // --- Keepalive ---
+    keepalive_pool: { size: 320, idle_timeout: 60, requests: 1000 },
+    // --- Health Checks (Structure needed for v-model) ---
+    checks: {
+      active: {
+        type: 'http',
+        http_path: '/',
+        host: undefined,
+        port: undefined,
+        timeout: 1,
+        concurrency: 10,
+        https_verify_certificate: true,
+        req_headers: [],
+        healthy: {
+          interval: 1,
+          successes: 2,
+          http_statuses: [200, 302],
+        },
+        unhealthy: {
+          interval: 1,
+          http_failures: 5,
+          tcp_failures: 2,
+          timeouts: 3,
+          http_statuses: [429, 404, 500, 501, 502, 503, 504, 505],
+        },
+      },
+    },
+  };
+}
+
+const performReset = () => {
+  Object.assign(localUpstreamData, getDefaultUpstreamData());
+  uiNodes.value = parseNodesToUi(undefined);
+  upstreamType.value = 'nodes';
+  isHealthCheckActive.value = false;
+};
+
+// --- Watchers for Data Synchronization ---
+
+// Watch incoming modelValue to update local state and UI nodes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal === null || newVal === undefined) {
+      performReset();
+      return;
+    }
+
+    const incoming: Record<string, any> = newVal;
+    const defaults = getDefaultUpstreamData();
+
+    // Determine upstream type (nodes vs discovery)
+    if (incoming.discovery_type) {
+      upstreamType.value = 'discovery';
+    } else {
+      upstreamType.value = 'nodes';
+    }
+
+    // Merge top-level fields
+    Object.assign(localUpstreamData, {
+      ...defaults,
+      ...incoming,
+      // Ensure nested objects are properly merged/initialized
+      timeout: { ...defaults.timeout, ...(incoming.timeout || {}) },
+      keepalive_pool: { ...defaults.keepalive_pool, ...(incoming.keepalive_pool || {}) },
+      // Deep merge checks, ensuring structure exists
+      checks: deepMergeChecks(defaults.checks, incoming.checks),
+    });
+
+    // Convert API nodes format to UI format
+    if (upstreamType.value === 'nodes') {
+      uiNodes.value = parseNodesToUi(incoming.nodes as Record<string, number> | undefined);
+      localUpstreamData.nodes = incoming.nodes;
+    } else {
+      uiNodes.value = [{ host: '', port: null, weight: 1 }];
+      localUpstreamData.nodes = undefined;
+    }
+
+    isHealthCheckActive.value = !!localUpstreamData.checks?.active;
+
+    if (isHealthCheckActive.value && !localUpstreamData.checks?.active) {
+      if (!localUpstreamData.checks) {
+        localUpstreamData.checks = {};
+      }
+      localUpstreamData.checks.active = { ...defaults.checks.active };
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: if条件疑似无效代码
created_at: 2025-05-01T23:45:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-select
+          v-model="localUpstreamData.pass_host"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-option value="pass" :label="t('components.upstreamForm.discovery.hostHeaderPass')" />
+          <t-option value="node" :label="t('components.upstreamForm.discovery.hostHeaderNode')" />
+          <t-option value="rewrite" :label="t('components.upstreamForm.discovery.hostHeaderRewrite')" />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        v-if="localUpstreamData.pass_host === 'rewrite'"
+        :label="t('components.upstreamForm.discovery.upstreamHostLabel')"
+        name="upstream_host"
+      >
+        <t-input
+          v-model="localUpstreamData.upstream_host"
+          :placeholder="t('components.upstreamForm.discovery.upstreamHostPlaceholder')"
+        />
+      </t-form-item>
+    </div>
+
+    <!-- 3. Retries and Retry Timeout -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retries.label')" name="retries">
+          <t-input-number
+            v-model="localUpstreamData.retries"
+            :placeholder="t('components.upstreamForm.retries.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retryTimeout.label')" name="retry_timeout">
+          <t-input-number
+            v-model="localUpstreamData.retry_timeout"
+            :placeholder="t('components.upstreamForm.retryTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 4. Protocol -->
+    <t-form-item :label="t('components.upstreamForm.protocol.label')" name="scheme" required-mark>
+      <t-select v-model="localUpstreamData.scheme" :placeholder="t('components.upstreamForm.protocol.placeholder')">
+        <t-option v-for="item in PROTOCOL_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 5. Timeout Settings -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectTimeout.label')" name="timeout.connect">
+          <t-input-number
+            v-model="localUpstreamData.timeout.connect"
+            :placeholder="t('components.upstreamForm.connectTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.sendTimeout.label')" name="timeout.send">
+          <t-input-number
+            v-model="localUpstreamData.timeout.send"
+            :placeholder="t('components.upstreamForm.sendTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.receiveTimeout.label')" name="timeout.read">
+          <t-input-number
+            v-model="localUpstreamData.timeout.read"
+            :placeholder="t('components.upstreamForm.receiveTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 6. Connection Pool -->
+    <t-divider>{{ t('components.upstreamForm.connectionPool.title') }}</t-divider>
+    <t-row :gutter="[16, 16]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectionPool.capacity.label')" name="keepalive_pool.size">
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.size"
+            :placeholder="t('components.upstreamForm.connectionPool.capacity.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.idleTimeout.label')"
+          name="keepalive_pool.idle_timeout"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.idle_timeout"
+            :placeholder="t('components.upstreamForm.connectionPool.idleTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.maxRequests.label')"
+          name="keepalive_pool.requests"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.requests"
+            :placeholder="t('components.upstreamForm.connectionPool.maxRequests.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 7. Health Checks -->
+    <t-divider>{{ t('components.upstreamForm.healthCheck.title') }}</t-divider>
+    <t-form-item :label="t('components.upstreamForm.healthCheck.active.enableLabel')" name="healthCheckActiveEnabled">
+      <t-switch v-model="isHealthCheckActive" />
+    </t-form-item>
+
+    <div v-if="isHealthCheckActive">
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.healthCheckType')"
+        name="checks.active.type"
+        required-mark
+      >
+        <t-select v-model="localUpstreamData.checks.active.type">
+          <t-option
+            v-for="item in ACTIVE_HEALTH_CHECK_TYPES"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+          />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.httpPathLabel')"
+        name="checks.active.http_path"
+        required-mark
+      >
+        <t-input
+          v-model="localUpstreamData.checks.active.http_path"
+          :placeholder="t('components.upstreamForm.healthCheck.active.httpPathPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.hostLabel')" name="checks.active.host">
+        <t-input
+          v-model="localUpstreamData.checks.active.host"
+          :placeholder="t('components.upstreamForm.healthCheck.active.hostPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.portLabel')" name="checks.active.port">
+        <t-input-number
+          v-model="localUpstreamData.checks.active.port"
+          :placeholder="t('components.upstreamForm.healthCheck.active.portPlaceholder')"
+          theme="normal"
+          :min="1"
+          :max="65535"
+          style="width: 100%"
+        />
+      </t-form-item>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.timeoutLabel')"
+            name="checks.active.timeout"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.timeout"
+              :placeholder="t('components.upstreamForm.healthCheck.active.timeoutPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.concurrencyLabel')"
+            name="checks.active.concurrency"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.concurrency"
+              :placeholder="t('components.upstreamForm.healthCheck.active.concurrencyPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-form-item
+        v-if="localUpstreamData.checks.active.type === 'https'"
+        :label="t('components.upstreamForm.healthCheck.active.httpsVerifyCertificateLabel')"
+        name="checks.active.https_verify_certificate"
+      >
+        <t-switch v-model="localUpstreamData.checks.active.https_verify_certificate" />
+      </t-form-item>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.healthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.intervalLabel')"
+            name="checks.active.healthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.successesLabel')"
+            name="checks.active.healthy.successes"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.successes']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.successes"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.successesPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesLabel')"
+            name="checks.active.healthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.healthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.unhealthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.intervalLabel')"
+            name="checks.active.unhealthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresLabel')"
+            name="checks.active.unhealthy.http_failures"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.http_failures']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.http_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            v-if="localUpstreamData.checks.active.type === 'tcp'"
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresLabel')"
+            name="checks.active.unhealthy.tcp_failures"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.tcp_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              :max="254"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesLabel')"
+            name="checks.active.unhealthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.unhealthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+    </div>
+  </t-form>
+</template>
+
+<script setup lang="ts">
+import { FormInstanceFunctions, MessagePlugin } from 'tdesign-vue-next';
+import { reactive, ref, watch } from 'vue';
+
+import NumberTagInput from '@/components/number-tag-input/index.vue';
+import { t } from '@/locales';
+
+import {
+  ACTIVE_HEALTH_CHECK_TYPES,
+  DISCOVERY_TYPE_OPTIONS,
+  HEALTH_CHECK_RULES,
+  LOAD_BALANCER_OPTIONS,
+  PROTOCOL_OPTIONS,
+} from './constants';
+
+interface UiNode {
+  host: string;
+  port: number | null;
+  weight: number;
+}
+
+// Define props for v-model
+// Use Record<string, any> to accept a generic key-value object
+const props = defineProps<{
+  modelValue: Record<string, any> | undefined | null;
+}>();
+
+// --- Internal State ---
+const formRef = ref<FormInstanceFunctions>();
+const isHealthCheckActive = ref(false); // Controls visibility and data structure of health checks
+const upstreamType = ref<'nodes' | 'discovery'>('nodes'); // Controls UI for nodes vs discovery
+
+// Internal representation for UI nodes using the local UiNode interface
+const uiNodes = ref<UiNode[]>([{ host: '', port: null, weight: 1 }]);
+
+const localUpstreamData = reactive<Record<string, any>>(getDefaultUpstreamData());
+
+// --- Helper Functions ---
+
+// Parses APISIX nodes object {"host:port": weight} into UI array [{host, port, weight}]
+// Returns the local UiNode type
+function parseNodesToUi(apiNodes: Record<string, number> | undefined): UiNode[] {
+  if (!apiNodes || typeof apiNodes !== 'object' || Object.keys(apiNodes).length === 0) {
+    return [{ host: '', port: null, weight: 1 }]; // Return default if empty or invalid
+  }
+  const parsed: UiNode[] = [];
+  for (const key in apiNodes) {
+    if (Object.prototype.hasOwnProperty.call(apiNodes, key)) {
+      const weight = apiNodes[key];
+      const parts = key.split(':');
+      const host = parts[0];
+      const port = parts.length > 1 ? parseInt(parts[1], 10) : null;
+
+      if (host && port !== null && !Number.isNaN(port) && typeof weight === 'number') {
+        parsed.push({ host, port, weight });
+      } else {
+        console.warn(`[UpstreamForm] Skipping invalid node entry: ${key}:${weight}`);
+        MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+      }
+    }
+  }
+  return parsed.length > 0 ? parsed : [{ host: '', port: null, weight: 1 }]; // Ensure at least one row if parsing resulted in empty
+}
+
+// Converts UI array [{host, port, weight}] back to APISIX object {"host:port": weight}
+// Accepts the local UiNode type
+function convertNodesToApi(nodesArray: UiNode[]): Record<string, number> | undefined {
+  const apiNodes: Record<string, number> = {};
+  let validNodes = 0;
+  nodesArray.forEach((node) => {
+    if (node.host && node.port && node.weight !== null && node.weight >= 0) {
+      apiNodes[`${node.host}:${node.port}`] = node.weight;
+      validNodes++;
+    }
+  });
+  // Return undefined if no valid nodes to avoid sending empty {}
+  return validNodes > 0 ? apiNodes : undefined;
+}
+
+// Gets default structure, ensuring nested objects exist for v-model bindings
+// Returns a plain object structure (Record<string, any>)
+function getDefaultUpstreamData(): Record<string, any> {
+  return {
+    type: 'roundrobin',
+    nodes: undefined, // Will be populated by conversion or default
+    // --- Discovery ---
+    discovery_type: undefined,
+    service_name: undefined,
+    pass_host: 'pass',
+    upstream_host: undefined,
+    // --- Retries ---
+    retries: undefined,
+    retry_timeout: undefined,
+    // --- Protocol & Timeouts ---
+    scheme: 'http',
+    timeout: { connect: 15, send: 15, read: 15 },
+    // --- Keepalive ---
+    keepalive_pool: { size: 320, idle_timeout: 60, requests: 1000 },
+    // --- Health Checks (Structure needed for v-model) ---
+    checks: {
+      active: {
+        type: 'http',
+        http_path: '/',
+        host: undefined,
+        port: undefined,
+        timeout: 1,
+        concurrency: 10,
+        https_verify_certificate: true,
+        req_headers: [],
+        healthy: {
+          interval: 1,
+          successes: 2,
+          http_statuses: [200, 302],
+        },
+        unhealthy: {
+          interval: 1,
+          http_failures: 5,
+          tcp_failures: 2,
+          timeouts: 3,
+          http_statuses: [429, 404, 500, 501, 502, 503, 504, 505],
+        },
+      },
+    },
+  };
+}
+
+const performReset = () => {
+  Object.assign(localUpstreamData, getDefaultUpstreamData());
+  uiNodes.value = parseNodesToUi(undefined);
+  upstreamType.value = 'nodes';
+  isHealthCheckActive.value = false;
+};
+
+// --- Watchers for Data Synchronization ---
+
+// Watch incoming modelValue to update local state and UI nodes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal === null || newVal === undefined) {
+      performReset();
+      return;
+    }
+
+    const incoming: Record<string, any> = newVal;
+    const defaults = getDefaultUpstreamData();
+
+    // Determine upstream type (nodes vs discovery)
+    if (incoming.discovery_type) {
+      upstreamType.value = 'discovery';
+    } else {
+      upstreamType.value = 'nodes';
+    }
+
+    // Merge top-level fields
+    Object.assign(localUpstreamData, {
+      ...defaults,
+      ...incoming,
+      // Ensure nested objects are properly merged/initialized
+      timeout: { ...defaults.timeout, ...(incoming.timeout || {}) },
+      keepalive_pool: { ...defaults.keepalive_pool, ...(incoming.keepalive_pool || {}) },
+      // Deep merge checks, ensuring structure exists
+      checks: deepMergeChecks(defaults.checks, incoming.checks),
+    });
+
+    // Convert API nodes format to UI format
+    if (upstreamType.value === 'nodes') {
+      uiNodes.value = parseNodesToUi(incoming.nodes as Record<string, number> | undefined);
+      localUpstreamData.nodes = incoming.nodes;
+    } else {
+      uiNodes.value = [{ host: '', port: null, weight: 1 }];
+      localUpstreamData.nodes = undefined;
+    }
+
+    isHealthCheckActive.value = !!localUpstreamData.checks?.active;
+
+    if (isHealthCheckActive.value && !localUpstreamData.checks?.active) {
+      if (!localUpstreamData.checks) {
+        localUpstreamData.checks = {};
+      }
+      localUpstreamData.checks.active = { ...defaults.checks.active };
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: 另外感觉这里的代码和deepMergeChecks是不是重复了
created_at: 2025-05-01T23:52:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-select
+          v-model="localUpstreamData.pass_host"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-option value="pass" :label="t('components.upstreamForm.discovery.hostHeaderPass')" />
+          <t-option value="node" :label="t('components.upstreamForm.discovery.hostHeaderNode')" />
+          <t-option value="rewrite" :label="t('components.upstreamForm.discovery.hostHeaderRewrite')" />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        v-if="localUpstreamData.pass_host === 'rewrite'"
+        :label="t('components.upstreamForm.discovery.upstreamHostLabel')"
+        name="upstream_host"
+      >
+        <t-input
+          v-model="localUpstreamData.upstream_host"
+          :placeholder="t('components.upstreamForm.discovery.upstreamHostPlaceholder')"
+        />
+      </t-form-item>
+    </div>
+
+    <!-- 3. Retries and Retry Timeout -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retries.label')" name="retries">
+          <t-input-number
+            v-model="localUpstreamData.retries"
+            :placeholder="t('components.upstreamForm.retries.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retryTimeout.label')" name="retry_timeout">
+          <t-input-number
+            v-model="localUpstreamData.retry_timeout"
+            :placeholder="t('components.upstreamForm.retryTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 4. Protocol -->
+    <t-form-item :label="t('components.upstreamForm.protocol.label')" name="scheme" required-mark>
+      <t-select v-model="localUpstreamData.scheme" :placeholder="t('components.upstreamForm.protocol.placeholder')">
+        <t-option v-for="item in PROTOCOL_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 5. Timeout Settings -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectTimeout.label')" name="timeout.connect">
+          <t-input-number
+            v-model="localUpstreamData.timeout.connect"
+            :placeholder="t('components.upstreamForm.connectTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.sendTimeout.label')" name="timeout.send">
+          <t-input-number
+            v-model="localUpstreamData.timeout.send"
+            :placeholder="t('components.upstreamForm.sendTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.receiveTimeout.label')" name="timeout.read">
+          <t-input-number
+            v-model="localUpstreamData.timeout.read"
+            :placeholder="t('components.upstreamForm.receiveTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 6. Connection Pool -->
+    <t-divider>{{ t('components.upstreamForm.connectionPool.title') }}</t-divider>
+    <t-row :gutter="[16, 16]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectionPool.capacity.label')" name="keepalive_pool.size">
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.size"
+            :placeholder="t('components.upstreamForm.connectionPool.capacity.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.idleTimeout.label')"
+          name="keepalive_pool.idle_timeout"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.idle_timeout"
+            :placeholder="t('components.upstreamForm.connectionPool.idleTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.maxRequests.label')"
+          name="keepalive_pool.requests"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.requests"
+            :placeholder="t('components.upstreamForm.connectionPool.maxRequests.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 7. Health Checks -->
+    <t-divider>{{ t('components.upstreamForm.healthCheck.title') }}</t-divider>
+    <t-form-item :label="t('components.upstreamForm.healthCheck.active.enableLabel')" name="healthCheckActiveEnabled">
+      <t-switch v-model="isHealthCheckActive" />
+    </t-form-item>
+
+    <div v-if="isHealthCheckActive">
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.healthCheckType')"
+        name="checks.active.type"
+        required-mark
+      >
+        <t-select v-model="localUpstreamData.checks.active.type">
+          <t-option
+            v-for="item in ACTIVE_HEALTH_CHECK_TYPES"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+          />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.httpPathLabel')"
+        name="checks.active.http_path"
+        required-mark
+      >
+        <t-input
+          v-model="localUpstreamData.checks.active.http_path"
+          :placeholder="t('components.upstreamForm.healthCheck.active.httpPathPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.hostLabel')" name="checks.active.host">
+        <t-input
+          v-model="localUpstreamData.checks.active.host"
+          :placeholder="t('components.upstreamForm.healthCheck.active.hostPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.portLabel')" name="checks.active.port">
+        <t-input-number
+          v-model="localUpstreamData.checks.active.port"
+          :placeholder="t('components.upstreamForm.healthCheck.active.portPlaceholder')"
+          theme="normal"
+          :min="1"
+          :max="65535"
+          style="width: 100%"
+        />
+      </t-form-item>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.timeoutLabel')"
+            name="checks.active.timeout"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.timeout"
+              :placeholder="t('components.upstreamForm.healthCheck.active.timeoutPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.concurrencyLabel')"
+            name="checks.active.concurrency"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.concurrency"
+              :placeholder="t('components.upstreamForm.healthCheck.active.concurrencyPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-form-item
+        v-if="localUpstreamData.checks.active.type === 'https'"
+        :label="t('components.upstreamForm.healthCheck.active.httpsVerifyCertificateLabel')"
+        name="checks.active.https_verify_certificate"
+      >
+        <t-switch v-model="localUpstreamData.checks.active.https_verify_certificate" />
+      </t-form-item>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.healthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.intervalLabel')"
+            name="checks.active.healthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.successesLabel')"
+            name="checks.active.healthy.successes"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.successes']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.successes"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.successesPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesLabel')"
+            name="checks.active.healthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.healthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.unhealthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.intervalLabel')"
+            name="checks.active.unhealthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresLabel')"
+            name="checks.active.unhealthy.http_failures"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.http_failures']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.http_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            v-if="localUpstreamData.checks.active.type === 'tcp'"
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresLabel')"
+            name="checks.active.unhealthy.tcp_failures"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.tcp_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              :max="254"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesLabel')"
+            name="checks.active.unhealthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.unhealthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+    </div>
+  </t-form>
+</template>
+
+<script setup lang="ts">
+import { FormInstanceFunctions, MessagePlugin } from 'tdesign-vue-next';
+import { reactive, ref, watch } from 'vue';
+
+import NumberTagInput from '@/components/number-tag-input/index.vue';
+import { t } from '@/locales';
+
+import {
+  ACTIVE_HEALTH_CHECK_TYPES,
+  DISCOVERY_TYPE_OPTIONS,
+  HEALTH_CHECK_RULES,
+  LOAD_BALANCER_OPTIONS,
+  PROTOCOL_OPTIONS,
+} from './constants';
+
+interface UiNode {
+  host: string;
+  port: number | null;
+  weight: number;
+}
+
+// Define props for v-model
+// Use Record<string, any> to accept a generic key-value object
+const props = defineProps<{
+  modelValue: Record<string, any> | undefined | null;
+}>();
+
+// --- Internal State ---
+const formRef = ref<FormInstanceFunctions>();
+const isHealthCheckActive = ref(false); // Controls visibility and data structure of health checks
+const upstreamType = ref<'nodes' | 'discovery'>('nodes'); // Controls UI for nodes vs discovery
+
+// Internal representation for UI nodes using the local UiNode interface
+const uiNodes = ref<UiNode[]>([{ host: '', port: null, weight: 1 }]);
+
+const localUpstreamData = reactive<Record<string, any>>(getDefaultUpstreamData());
+
+// --- Helper Functions ---
+
+// Parses APISIX nodes object {"host:port": weight} into UI array [{host, port, weight}]
+// Returns the local UiNode type
+function parseNodesToUi(apiNodes: Record<string, number> | undefined): UiNode[] {
+  if (!apiNodes || typeof apiNodes !== 'object' || Object.keys(apiNodes).length === 0) {
+    return [{ host: '', port: null, weight: 1 }]; // Return default if empty or invalid
+  }
+  const parsed: UiNode[] = [];
+  for (const key in apiNodes) {
+    if (Object.prototype.hasOwnProperty.call(apiNodes, key)) {
+      const weight = apiNodes[key];
+      const parts = key.split(':');
+      const host = parts[0];
+      const port = parts.length > 1 ? parseInt(parts[1], 10) : null;
+
+      if (host && port !== null && !Number.isNaN(port) && typeof weight === 'number') {
+        parsed.push({ host, port, weight });
+      } else {
+        console.warn(`[UpstreamForm] Skipping invalid node entry: ${key}:${weight}`);
+        MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+      }
+    }
+  }
+  return parsed.length > 0 ? parsed : [{ host: '', port: null, weight: 1 }]; // Ensure at least one row if parsing resulted in empty
+}
+
+// Converts UI array [{host, port, weight}] back to APISIX object {"host:port": weight}
+// Accepts the local UiNode type
+function convertNodesToApi(nodesArray: UiNode[]): Record<string, number> | undefined {
+  const apiNodes: Record<string, number> = {};
+  let validNodes = 0;
+  nodesArray.forEach((node) => {
+    if (node.host && node.port && node.weight !== null && node.weight >= 0) {
+      apiNodes[`${node.host}:${node.port}`] = node.weight;
+      validNodes++;
+    }
+  });
+  // Return undefined if no valid nodes to avoid sending empty {}
+  return validNodes > 0 ? apiNodes : undefined;
+}
+
+// Gets default structure, ensuring nested objects exist for v-model bindings
+// Returns a plain object structure (Record<string, any>)
+function getDefaultUpstreamData(): Record<string, any> {
+  return {
+    type: 'roundrobin',
+    nodes: undefined, // Will be populated by conversion or default
+    // --- Discovery ---
+    discovery_type: undefined,
+    service_name: undefined,
+    pass_host: 'pass',
+    upstream_host: undefined,
+    // --- Retries ---
+    retries: undefined,
+    retry_timeout: undefined,
+    // --- Protocol & Timeouts ---
+    scheme: 'http',
+    timeout: { connect: 15, send: 15, read: 15 },
+    // --- Keepalive ---
+    keepalive_pool: { size: 320, idle_timeout: 60, requests: 1000 },
+    // --- Health Checks (Structure needed for v-model) ---
+    checks: {
+      active: {
+        type: 'http',
+        http_path: '/',
+        host: undefined,
+        port: undefined,
+        timeout: 1,
+        concurrency: 10,
+        https_verify_certificate: true,
+        req_headers: [],
+        healthy: {
+          interval: 1,
+          successes: 2,
+          http_statuses: [200, 302],
+        },
+        unhealthy: {
+          interval: 1,
+          http_failures: 5,
+          tcp_failures: 2,
+          timeouts: 3,
+          http_statuses: [429, 404, 500, 501, 502, 503, 504, 505],
+        },
+      },
+    },
+  };
+}
+
+const performReset = () => {
+  Object.assign(localUpstreamData, getDefaultUpstreamData());
+  uiNodes.value = parseNodesToUi(undefined);
+  upstreamType.value = 'nodes';
+  isHealthCheckActive.value = false;
+};
+
+// --- Watchers for Data Synchronization ---
+
+// Watch incoming modelValue to update local state and UI nodes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal === null || newVal === undefined) {
+      performReset();
+      return;
+    }
+
+    const incoming: Record<string, any> = newVal;
+    const defaults = getDefaultUpstreamData();
+
+    // Determine upstream type (nodes vs discovery)
+    if (incoming.discovery_type) {
+      upstreamType.value = 'discovery';
+    } else {
+      upstreamType.value = 'nodes';
+    }
+
+    // Merge top-level fields
+    Object.assign(localUpstreamData, {
+      ...defaults,
+      ...incoming,
+      // Ensure nested objects are properly merged/initialized
+      timeout: { ...defaults.timeout, ...(incoming.timeout || {}) },
+      keepalive_pool: { ...defaults.keepalive_pool, ...(incoming.keepalive_pool || {}) },
+      // Deep merge checks, ensuring structure exists
+      checks: deepMergeChecks(defaults.checks, incoming.checks),
+    });
+
+    // Convert API nodes format to UI format
+    if (upstreamType.value === 'nodes') {
+      uiNodes.value = parseNodesToUi(incoming.nodes as Record<string, number> | undefined);
+      localUpstreamData.nodes = incoming.nodes;
+    } else {
+      uiNodes.value = [{ host: '', port: null, weight: 1 }];
+      localUpstreamData.nodes = undefined;
+    }
+
+    isHealthCheckActive.value = !!localUpstreamData.checks?.active;
+
+    if (isHealthCheckActive.value && !localUpstreamData.checks?.active) {
+      if (!localUpstreamData.checks) {
+        localUpstreamData.checks = {};
+      }
+      localUpstreamData.checks.active = { ...defaults.checks.active };
+    } else if (isHealthCheckActive.value && localUpstreamData.checks?.active) {
+      localUpstreamData.checks.active.healthy = {
+        ...defaults.checks.active.healthy,
+        ...(localUpstreamData.checks.active.healthy || {}),
+      };
+      localUpstreamData.checks.active.unhealthy = {
+        ...defaults.checks.active.unhealthy,
+        ...(localUpstreamData.checks.active.unhealthy || {}),
+      };
+    }
+  },
+  { immediate: true, deep: false }, // Use deep: false initially, rely on specific watchers below for deep changes
+);
+
+// Helper for deep merging checks structure (accepts/returns 'any' for flexibility)
+function deepMergeChecks(defaults: any, incoming: any): any {
+  if (!incoming) return defaults;
+  const merged = { ...defaults };
+  if (incoming.active) {
+    merged.active = {
+      ...(defaults.active || {}),
+      ...incoming.active,
+      healthy: { ...(defaults.active?.healthy || {}), ...(incoming.active.healthy || {}) },
+      unhealthy: { ...(defaults.active?.unhealthy || {}), ...(incoming.active.unhealthy || {}) },
+    };
+  }
+  // TODO(icebound): Add merging for passive checks here.
+  // if (incoming.passive) { ... }
+  return merged;
+}
+
+// --- UI Interaction Methods ---
+
+const addNode = () => {
+  uiNodes.value.push({ host: '', port: null, weight: 1 });
+};
+
+const removeNode = (index: number) => {
+  if (uiNodes.value.length > 1) {
+    uiNodes.value.splice(index, 1);
+  }
+};
+
+// --- Expose Methods ---
+defineExpose({
+  validate: () => {
+    if (upstreamType.value === 'nodes') {
+      // Validate each node's host, port, and weight
+      for (const node of uiNodes.value) {
+        if (!node.host || !node.port || node.weight === null || node.weight < 0) {
+          MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+          return false;
+        }
+      }
+    }
+    return formRef.value?.validate();
+  },
+  getApiFormattedData: (): Record<string, any> => {
+    const result: Record<string, any> = JSON.parse(JSON.stringify(localUpstreamData));
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: lodash cloneDeep
created_at: 2025-05-01T23:56:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,734 @@
+<template>
+  <t-form ref="formRef" :data="localUpstreamData" label-align="top" :colon="true">
+    <!-- 1. Load Balancer Algorithm -->
+    <t-form-item :label="t('components.upstreamForm.loadBalancer.label')" name="type">
+      <t-select
+        v-model="localUpstreamData.type"
+        :placeholder="t('components.upstreamForm.loadBalancer.placeholder')"
+        clearable
+      >
+        <t-option v-for="item in LOAD_BALANCER_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 2. Upstream Type (Nodes or Discovery) -->
+    <t-form-item :label="t('components.upstreamForm.upstreamType.label')" name="upstreamTypeSelector" required-mark>
+      <t-radio-group v-model="upstreamType">
+        <t-radio value="nodes">{{ t('components.upstreamForm.upstreamType.nodes') }}</t-radio>
+        <t-radio value="discovery">{{ t('components.upstreamForm.upstreamType.discovery') }}</t-radio>
+      </t-radio-group>
+    </t-form-item>
+
+    <!-- 2a. Nodes Configuration -->
+    <div v-if="upstreamType === 'nodes'">
+      <t-form-item
+        v-for="(node, index) in uiNodes"
+        :key="index"
+        :label="index === 0 ? t('components.upstreamForm.nodes.label') : ''"
+        :name="`uiNodes[${index}]`"
+        class="node-item"
+      >
+        <div class="node-row">
+          <span class="node-label">{{ t('components.upstreamForm.nodes.hostLabel') }}</span>
+          <t-input
+            v-model="node.host"
+            class="node-input"
+            :placeholder="t('components.upstreamForm.nodes.hostPlaceholder')"
+            required-mark
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.portLabel') }}</span>
+          <t-input-number
+            v-model="node.port"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.portPlaceholder')"
+            :min="1"
+            :max="65535"
+            required-mark
+            theme="normal"
+          />
+          <span class="node-label">{{ t('components.upstreamForm.nodes.weightLabel') }}</span>
+          <t-input-number
+            v-model="node.weight"
+            class="node-input-number"
+            :placeholder="t('components.upstreamForm.nodes.weightPlaceholder')"
+            :min="0"
+            required-mark
+            theme="normal"
+          />
+          <t-button
+            v-if="uiNodes.length > 1"
+            shape="circle"
+            variant="text"
+            theme="danger"
+            class="remove-button"
+            @click="removeNode(index)"
+          >
+            <t-icon name="remove" />
+          </t-button>
+        </div>
+      </t-form-item>
+      <t-form-item>
+        <t-button variant="dashed" @click="addNode">
+          <template #icon><t-icon name="add" /></template>
+          {{ t('components.upstreamForm.nodes.addNode') }}
+        </t-button>
+      </t-form-item>
+    </div>
+
+    <!-- 2b. Discovery Configuration -->
+    <div v-if="upstreamType === 'discovery'">
+      <t-form-item :label="t('components.upstreamForm.discovery.typeLabel')" name="discovery_type" required-mark>
+        <t-select
+          v-model="localUpstreamData.discovery_type"
+          :placeholder="t('components.upstreamForm.discovery.typePlaceholder')"
+          clearable
+        >
+          <t-option v-for="item in DISCOVERY_TYPE_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+        </t-select>
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.serviceNameLabel')" name="service_name" required-mark>
+        <t-input
+          v-model="localUpstreamData.service_name"
+          :placeholder="t('components.upstreamForm.discovery.serviceNamePlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.discovery.hostHeaderLabel')" name="pass_host">
+        <t-select
+          v-model="localUpstreamData.pass_host"
+          :placeholder="t('components.upstreamForm.discovery.hostHeaderPlaceholder')"
+        >
+          <t-option value="pass" :label="t('components.upstreamForm.discovery.hostHeaderPass')" />
+          <t-option value="node" :label="t('components.upstreamForm.discovery.hostHeaderNode')" />
+          <t-option value="rewrite" :label="t('components.upstreamForm.discovery.hostHeaderRewrite')" />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        v-if="localUpstreamData.pass_host === 'rewrite'"
+        :label="t('components.upstreamForm.discovery.upstreamHostLabel')"
+        name="upstream_host"
+      >
+        <t-input
+          v-model="localUpstreamData.upstream_host"
+          :placeholder="t('components.upstreamForm.discovery.upstreamHostPlaceholder')"
+        />
+      </t-form-item>
+    </div>
+
+    <!-- 3. Retries and Retry Timeout -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retries.label')" name="retries">
+          <t-input-number
+            v-model="localUpstreamData.retries"
+            :placeholder="t('components.upstreamForm.retries.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="6">
+        <t-form-item :label="t('components.upstreamForm.retryTimeout.label')" name="retry_timeout">
+          <t-input-number
+            v-model="localUpstreamData.retry_timeout"
+            :placeholder="t('components.upstreamForm.retryTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 4. Protocol -->
+    <t-form-item :label="t('components.upstreamForm.protocol.label')" name="scheme" required-mark>
+      <t-select v-model="localUpstreamData.scheme" :placeholder="t('components.upstreamForm.protocol.placeholder')">
+        <t-option v-for="item in PROTOCOL_OPTIONS" :key="item.value" :value="item.value" :label="item.label" />
+      </t-select>
+    </t-form-item>
+
+    <!-- 5. Timeout Settings -->
+    <t-row :gutter="[16, 0]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectTimeout.label')" name="timeout.connect">
+          <t-input-number
+            v-model="localUpstreamData.timeout.connect"
+            :placeholder="t('components.upstreamForm.connectTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.sendTimeout.label')" name="timeout.send">
+          <t-input-number
+            v-model="localUpstreamData.timeout.send"
+            :placeholder="t('components.upstreamForm.sendTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.receiveTimeout.label')" name="timeout.read">
+          <t-input-number
+            v-model="localUpstreamData.timeout.read"
+            :placeholder="t('components.upstreamForm.receiveTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 6. Connection Pool -->
+    <t-divider>{{ t('components.upstreamForm.connectionPool.title') }}</t-divider>
+    <t-row :gutter="[16, 16]">
+      <t-col :span="4">
+        <t-form-item :label="t('components.upstreamForm.connectionPool.capacity.label')" name="keepalive_pool.size">
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.size"
+            :placeholder="t('components.upstreamForm.connectionPool.capacity.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.idleTimeout.label')"
+          name="keepalive_pool.idle_timeout"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.idle_timeout"
+            :placeholder="t('components.upstreamForm.connectionPool.idleTimeout.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          >
+            <template #suffix>s</template>
+          </t-input-number>
+        </t-form-item>
+      </t-col>
+      <t-col :span="4">
+        <t-form-item
+          :label="t('components.upstreamForm.connectionPool.maxRequests.label')"
+          name="keepalive_pool.requests"
+        >
+          <t-input-number
+            v-model="localUpstreamData.keepalive_pool.requests"
+            :placeholder="t('components.upstreamForm.connectionPool.maxRequests.placeholder')"
+            theme="normal"
+            :min="0"
+            style="width: 100%"
+          />
+        </t-form-item>
+      </t-col>
+    </t-row>
+
+    <!-- 7. Health Checks -->
+    <t-divider>{{ t('components.upstreamForm.healthCheck.title') }}</t-divider>
+    <t-form-item :label="t('components.upstreamForm.healthCheck.active.enableLabel')" name="healthCheckActiveEnabled">
+      <t-switch v-model="isHealthCheckActive" />
+    </t-form-item>
+
+    <div v-if="isHealthCheckActive">
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.healthCheckType')"
+        name="checks.active.type"
+        required-mark
+      >
+        <t-select v-model="localUpstreamData.checks.active.type">
+          <t-option
+            v-for="item in ACTIVE_HEALTH_CHECK_TYPES"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+          />
+        </t-select>
+      </t-form-item>
+      <t-form-item
+        :label="t('components.upstreamForm.healthCheck.active.httpPathLabel')"
+        name="checks.active.http_path"
+        required-mark
+      >
+        <t-input
+          v-model="localUpstreamData.checks.active.http_path"
+          :placeholder="t('components.upstreamForm.healthCheck.active.httpPathPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.hostLabel')" name="checks.active.host">
+        <t-input
+          v-model="localUpstreamData.checks.active.host"
+          :placeholder="t('components.upstreamForm.healthCheck.active.hostPlaceholder')"
+        />
+      </t-form-item>
+      <t-form-item :label="t('components.upstreamForm.healthCheck.active.portLabel')" name="checks.active.port">
+        <t-input-number
+          v-model="localUpstreamData.checks.active.port"
+          :placeholder="t('components.upstreamForm.healthCheck.active.portPlaceholder')"
+          theme="normal"
+          :min="1"
+          :max="65535"
+          style="width: 100%"
+        />
+      </t-form-item>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.timeoutLabel')"
+            name="checks.active.timeout"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.timeout"
+              :placeholder="t('components.upstreamForm.healthCheck.active.timeoutPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.concurrencyLabel')"
+            name="checks.active.concurrency"
+            required-mark
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.concurrency"
+              :placeholder="t('components.upstreamForm.healthCheck.active.concurrencyPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-form-item
+        v-if="localUpstreamData.checks.active.type === 'https'"
+        :label="t('components.upstreamForm.healthCheck.active.httpsVerifyCertificateLabel')"
+        name="checks.active.https_verify_certificate"
+      >
+        <t-switch v-model="localUpstreamData.checks.active.https_verify_certificate" />
+      </t-form-item>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.healthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.intervalLabel')"
+            name="checks.active.healthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.successesLabel')"
+            name="checks.active.healthy.successes"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.healthy.successes']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.healthy.successes"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.successesPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesLabel')"
+            name="checks.active.healthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.healthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.healthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+
+      <t-divider align="left">{{ t('components.upstreamForm.healthCheck.active.unhealthy.title') }}</t-divider>
+      <t-row :gutter="[16, 0]">
+        <t-col :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.intervalLabel')"
+            name="checks.active.unhealthy.interval"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.interval']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.interval"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.intervalPlaceholder')"
+              theme="normal"
+              :min="0"
+              style="width: 100%"
+            >
+              <template #suffix>s</template>
+            </t-input-number>
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="6">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresLabel')"
+            name="checks.active.unhealthy.http_failures"
+            required-mark
+            :rules="HEALTH_CHECK_RULES['checks.active.unhealthy.http_failures']"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.http_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col :span="6">
+          <t-form-item
+            v-if="localUpstreamData.checks.active.type === 'tcp'"
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresLabel')"
+            name="checks.active.unhealthy.tcp_failures"
+          >
+            <t-input-number
+              v-model="localUpstreamData.checks.active.unhealthy.tcp_failures"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.tcpFailuresPlaceholder')"
+              theme="normal"
+              :min="1"
+              :max="254"
+              style="width: 100%"
+            />
+          </t-form-item>
+        </t-col>
+        <t-col v-if="localUpstreamData.checks.active.type != 'tcp'" :span="12">
+          <t-form-item
+            :label="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesLabel')"
+            name="checks.active.unhealthy.http_statuses"
+          >
+            <number-tag-input
+              v-model="localUpstreamData.checks.active.unhealthy.http_statuses"
+              :placeholder="t('components.upstreamForm.healthCheck.active.unhealthy.httpStatusesPlaceholder')"
+              :tag-props="{ theme: 'primary', variant: 'light' }"
+              excess-tags-display="scroll"
+              auto-width
+            />
+          </t-form-item>
+        </t-col>
+      </t-row>
+    </div>
+  </t-form>
+</template>
+
+<script setup lang="ts">
+import { FormInstanceFunctions, MessagePlugin } from 'tdesign-vue-next';
+import { reactive, ref, watch } from 'vue';
+
+import NumberTagInput from '@/components/number-tag-input/index.vue';
+import { t } from '@/locales';
+
+import {
+  ACTIVE_HEALTH_CHECK_TYPES,
+  DISCOVERY_TYPE_OPTIONS,
+  HEALTH_CHECK_RULES,
+  LOAD_BALANCER_OPTIONS,
+  PROTOCOL_OPTIONS,
+} from './constants';
+
+interface UiNode {
+  host: string;
+  port: number | null;
+  weight: number;
+}
+
+// Define props for v-model
+// Use Record<string, any> to accept a generic key-value object
+const props = defineProps<{
+  modelValue: Record<string, any> | undefined | null;
+}>();
+
+// --- Internal State ---
+const formRef = ref<FormInstanceFunctions>();
+const isHealthCheckActive = ref(false); // Controls visibility and data structure of health checks
+const upstreamType = ref<'nodes' | 'discovery'>('nodes'); // Controls UI for nodes vs discovery
+
+// Internal representation for UI nodes using the local UiNode interface
+const uiNodes = ref<UiNode[]>([{ host: '', port: null, weight: 1 }]);
+
+const localUpstreamData = reactive<Record<string, any>>(getDefaultUpstreamData());
+
+// --- Helper Functions ---
+
+// Parses APISIX nodes object {"host:port": weight} into UI array [{host, port, weight}]
+// Returns the local UiNode type
+function parseNodesToUi(apiNodes: Record<string, number> | undefined): UiNode[] {
+  if (!apiNodes || typeof apiNodes !== 'object' || Object.keys(apiNodes).length === 0) {
+    return [{ host: '', port: null, weight: 1 }]; // Return default if empty or invalid
+  }
+  const parsed: UiNode[] = [];
+  for (const key in apiNodes) {
+    if (Object.prototype.hasOwnProperty.call(apiNodes, key)) {
+      const weight = apiNodes[key];
+      const parts = key.split(':');
+      const host = parts[0];
+      const port = parts.length > 1 ? parseInt(parts[1], 10) : null;
+
+      if (host && port !== null && !Number.isNaN(port) && typeof weight === 'number') {
+        parsed.push({ host, port, weight });
+      } else {
+        console.warn(`[UpstreamForm] Skipping invalid node entry: ${key}:${weight}`);
+        MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+      }
+    }
+  }
+  return parsed.length > 0 ? parsed : [{ host: '', port: null, weight: 1 }]; // Ensure at least one row if parsing resulted in empty
+}
+
+// Converts UI array [{host, port, weight}] back to APISIX object {"host:port": weight}
+// Accepts the local UiNode type
+function convertNodesToApi(nodesArray: UiNode[]): Record<string, number> | undefined {
+  const apiNodes: Record<string, number> = {};
+  let validNodes = 0;
+  nodesArray.forEach((node) => {
+    if (node.host && node.port && node.weight !== null && node.weight >= 0) {
+      apiNodes[`${node.host}:${node.port}`] = node.weight;
+      validNodes++;
+    }
+  });
+  // Return undefined if no valid nodes to avoid sending empty {}
+  return validNodes > 0 ? apiNodes : undefined;
+}
+
+// Gets default structure, ensuring nested objects exist for v-model bindings
+// Returns a plain object structure (Record<string, any>)
+function getDefaultUpstreamData(): Record<string, any> {
+  return {
+    type: 'roundrobin',
+    nodes: undefined, // Will be populated by conversion or default
+    // --- Discovery ---
+    discovery_type: undefined,
+    service_name: undefined,
+    pass_host: 'pass',
+    upstream_host: undefined,
+    // --- Retries ---
+    retries: undefined,
+    retry_timeout: undefined,
+    // --- Protocol & Timeouts ---
+    scheme: 'http',
+    timeout: { connect: 15, send: 15, read: 15 },
+    // --- Keepalive ---
+    keepalive_pool: { size: 320, idle_timeout: 60, requests: 1000 },
+    // --- Health Checks (Structure needed for v-model) ---
+    checks: {
+      active: {
+        type: 'http',
+        http_path: '/',
+        host: undefined,
+        port: undefined,
+        timeout: 1,
+        concurrency: 10,
+        https_verify_certificate: true,
+        req_headers: [],
+        healthy: {
+          interval: 1,
+          successes: 2,
+          http_statuses: [200, 302],
+        },
+        unhealthy: {
+          interval: 1,
+          http_failures: 5,
+          tcp_failures: 2,
+          timeouts: 3,
+          http_statuses: [429, 404, 500, 501, 502, 503, 504, 505],
+        },
+      },
+    },
+  };
+}
+
+const performReset = () => {
+  Object.assign(localUpstreamData, getDefaultUpstreamData());
+  uiNodes.value = parseNodesToUi(undefined);
+  upstreamType.value = 'nodes';
+  isHealthCheckActive.value = false;
+};
+
+// --- Watchers for Data Synchronization ---
+
+// Watch incoming modelValue to update local state and UI nodes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal === null || newVal === undefined) {
+      performReset();
+      return;
+    }
+
+    const incoming: Record<string, any> = newVal;
+    const defaults = getDefaultUpstreamData();
+
+    // Determine upstream type (nodes vs discovery)
+    if (incoming.discovery_type) {
+      upstreamType.value = 'discovery';
+    } else {
+      upstreamType.value = 'nodes';
+    }
+
+    // Merge top-level fields
+    Object.assign(localUpstreamData, {
+      ...defaults,
+      ...incoming,
+      // Ensure nested objects are properly merged/initialized
+      timeout: { ...defaults.timeout, ...(incoming.timeout || {}) },
+      keepalive_pool: { ...defaults.keepalive_pool, ...(incoming.keepalive_pool || {}) },
+      // Deep merge checks, ensuring structure exists
+      checks: deepMergeChecks(defaults.checks, incoming.checks),
+    });
+
+    // Convert API nodes format to UI format
+    if (upstreamType.value === 'nodes') {
+      uiNodes.value = parseNodesToUi(incoming.nodes as Record<string, number> | undefined);
+      localUpstreamData.nodes = incoming.nodes;
+    } else {
+      uiNodes.value = [{ host: '', port: null, weight: 1 }];
+      localUpstreamData.nodes = undefined;
+    }
+
+    isHealthCheckActive.value = !!localUpstreamData.checks?.active;
+
+    if (isHealthCheckActive.value && !localUpstreamData.checks?.active) {
+      if (!localUpstreamData.checks) {
+        localUpstreamData.checks = {};
+      }
+      localUpstreamData.checks.active = { ...defaults.checks.active };
+    } else if (isHealthCheckActive.value && localUpstreamData.checks?.active) {
+      localUpstreamData.checks.active.healthy = {
+        ...defaults.checks.active.healthy,
+        ...(localUpstreamData.checks.active.healthy || {}),
+      };
+      localUpstreamData.checks.active.unhealthy = {
+        ...defaults.checks.active.unhealthy,
+        ...(localUpstreamData.checks.active.unhealthy || {}),
+      };
+    }
+  },
+  { immediate: true, deep: false }, // Use deep: false initially, rely on specific watchers below for deep changes
+);
+
+// Helper for deep merging checks structure (accepts/returns 'any' for flexibility)
+function deepMergeChecks(defaults: any, incoming: any): any {
+  if (!incoming) return defaults;
+  const merged = { ...defaults };
+  if (incoming.active) {
+    merged.active = {
+      ...(defaults.active || {}),
+      ...incoming.active,
+      healthy: { ...(defaults.active?.healthy || {}), ...(incoming.active.healthy || {}) },
+      unhealthy: { ...(defaults.active?.unhealthy || {}), ...(incoming.active.unhealthy || {}) },
+    };
+  }
+  // TODO(icebound): Add merging for passive checks here.
+  // if (incoming.passive) { ... }
+  return merged;
+}
+
+// --- UI Interaction Methods ---
+
+const addNode = () => {
+  uiNodes.value.push({ host: '', port: null, weight: 1 });
+};
+
+const removeNode = (index: number) => {
+  if (uiNodes.value.length > 1) {
+    uiNodes.value.splice(index, 1);
+  }
+};
+
+// --- Expose Methods ---
+defineExpose({
+  validate: () => {
+    if (upstreamType.value === 'nodes') {
+      // Validate each node's host, port, and weight
+      for (const node of uiNodes.value) {
+        if (!node.host || !node.port || node.weight === null || node.weight < 0) {
+          MessagePlugin.warning(t('components.upstreamForm.warnings.invalidNodeFormat'));
+          return false;
+        }
+      }
+    }
+    return formRef.value?.validate();
+  },
+  getApiFormattedData: (): Record<string, any> => {
+    const result: Record<string, any> = JSON.parse(JSON.stringify(localUpstreamData));
+
+    // Apply the same cleanup logic as in emitUpdate
+    if (upstreamType.value === 'nodes') {
+      delete result.discovery_type;
+      delete result.service_name;
+      result.nodes = convertNodesToApi(uiNodes.value);
+      if (!result.nodes) delete result.nodes;
+    } else {
+      delete result.nodes;
+      if (!result.discovery_type) delete result.service_name;
+    }
+    if (result.pass_host !== 'rewrite') delete result.upstream_host;
+
+    if (!isHealthCheckActive.value) {
+      delete result.checks;
+    } else if (result.checks?.active) {
+      // Ensure type exists if active checks are enabled
+      if (!result.checks.active.type) {
+        result.checks.active.type = 'http';
+      }
path: src/components/upstream/upstream-form.vue
user: login: Lensual
body: 我认为不该在输出时赋予默认值。默认值逻辑应该放置在输入和UI组件状态变更时。
这个方法应该只负责UI组件到Api接口数据类型的转换
created_at: 2025-05-02T00:03:39Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next'; // 假设可以导入类型
+import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next';
path: src/components/number-tag-input/index.vue
user: login: Lensual
body: > TagInput as TTagInput

如果只是在template中使用，这里其实是不需要的，在src/main.ts中已导入tdesign组件

另外按vue风格，template通常在script上面

我以为你注意到了这两点。。。
created_at: 2025-05-03T17:28:35Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next'; // 假设可以导入类型
+import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next';
path: src/components/number-tag-input/index.vue
user: login: iceboundx
body: sorry，ai 生成的，我改一下
created_at: 2025-05-04T08:15:35Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next'; // 假设可以导入类型
+import { TagInput as TTagInput, TagInputProps, TagProps } from 'tdesign-vue-next';
path: src/components/number-tag-input/index.vue
user: login: iceboundx
body: 后面再改吧，我先合入，拖太久了
created_at: 2025-05-04T08:16:07Z
author_association: COLLABORATOR

PR comments:
user: login: Lensual
created_at: 2025-05-02T00:16:02Z
body: 总体来说感觉`watch`和`getApiFormattedData`的思路有点混乱。动态部分可能拆分成子组件会更好
author_association: COLLABORATOR
user: login: iceboundx
created_at: 2025-05-03T13:39:42Z
body: > 总体来说感觉`watch`和`getApiFormattedData`的思路有点混乱。动态部分可能拆分成子组件会更好

第一次写，轻喷
author_association: COLLABORATOR