# 核心组件设计规范

> DDS-Section: Frontend DDS - 8. 组件设计规范
> DDS-Lines: L696-L783

## CopyableField 组件

用于展示可复制的字段值：

```html
<template>
  <div class="copyable-field d-flex align-center gap-2">
    <span class="field-value">{{ displayValue }}</span>
    <v-btn 
      icon="mdi-content-copy" 
      size="x-small" 
      variant="text"
      @click="copyToClipboard"
    >
      <v-tooltip activator="parent" location="top">复制</v-tooltip>
    </v-btn>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useClipboard } from '@vueuse/core'

const props = defineProps<{
  value: string
  mask?: boolean
}>()

const { copy } = useClipboard()

const displayValue = computed(() => {
  if (props.mask) return '******'
  return props.value
})

const copyToClipboard = () => {
  copy(props.value)
}
</script>
```

## SaveConfirmDialog 组件

保存前的确认对话框，展示变更差异：

```html
<v-dialog v-model="visible" max-width="600">
  <v-card>
    <v-card-title class="bg-primary text-white">
      <v-icon start>mdi-check-circle</v-icon>
      确认保存修改
    </v-card-title>
    <v-card-text>
      <v-alert type="info" variant="tonal" class="mb-4">
        以下字段将被修改：
      </v-alert>
      <v-table density="compact">
        <thead>
          <tr>
            <th>字段</th>
            <th>修改前</th>
            <th>修改后</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="item in diffItems" :key="item.field">
            <td>{{ item.label }}</td>
            <td class="text-error">{{ item.oldValue || '空' }}</td>
            <td class="text-success">{{ item.newValue || '空' }}</td>
          </tr>
        </tbody>
      </v-table>
    </v-card-text>
    <v-card-actions>
      <v-spacer />
      <v-btn variant="text" @click="cancel">取消</v-btn>
      <v-btn color="primary" :loading="loading" @click="confirm">确认保存</v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>
```

## DiffTextField 组件

编辑模式下显示与主线数据差异的输入框：

```html
<template>
  <v-text-field
    v-model="inputValue"
    :label="label"
    :class="{ 'diff-highlight': hasDiff }"
    :hint="hasDiff ? `主线值: ${masterValue}` : ''"
    persistent-hint
  >
    <template v-slot:prepend-inner v-if="hasDiff">
      <v-icon color="warning" size="small">mdi-alert-circle</v-icon>
    </template>
  </v-text-field>
</template>

<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{
  modelValue: string
  masterValue: string
  label: string
}>()

const emit = defineEmits(['update:modelValue'])

const inputValue = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})

const hasDiff = computed(() => props.modelValue !== props.masterValue)
</script>

<style scoped>
.diff-highlight :deep(.v-field__outline) {
  --v-field-border-color: rgb(var(--v-theme-warning));
}
</style>
```

## 组件清单

| 组件 | 文件路径 | 说明 |
|:---|:---|:---|
| BasicInfoForm | `components/BasicInfoForm.vue` | 基本信息编辑表单 |
| BasicInfoReadonly | `components/BasicInfoReadonly.vue` | 基本信息只读 |
| BusinessInfoReadonly | `components/BusinessInfoReadonly.vue` | 业务信息只读 |
| DeploymentBusinessForm | `components/DeploymentBusinessForm.vue` | 业务信息表单 |
| DeploymentEnvironmentForm | `components/DeploymentEnvironmentForm.vue` | 环境信息表单 |
| EnvironmentInfoReadonly | `components/EnvironmentInfoReadonly.vue` | 环境信息只读 |
| HostsInfoReadonly | `components/HostsInfoReadonly.vue` | 主机信息只读 |
| HostsManagement | `components/HostsManagement.vue` | 主机管理 |
| MiddlewareCardsGrid | `components/MiddlewareCardsGrid.vue` | 中间件卡片网格 |
| MiddlewareInfoReadonly | `components/MiddlewareInfoReadonly.vue` | 中间件只读 |
| AuthorizationManagement | `components/AuthorizationManagement.vue` | 授权管理 |
| VersionHistory | `components/VersionHistory.vue` | 版本历史 |
| SaveConfirmDialog | `components/SaveConfirmDialog.vue` | 保存确认对话框 |
| CopyableField | `components/CopyableField.vue` | 可复制字段 |
| ProjectBasicInfoCard | `components/ProjectBasicInfoCard.vue` | 项目基本信息卡片 |
