소스 검색

复用构建配置 Add 相关的组件

reghao 4 일 전
부모
커밋
37a762c46e

+ 6 - 2
src/api/devops.js

@@ -93,6 +93,10 @@ export function deleteMachine(payload) {
   return postForm(devopsApi.getMachineList + '/delete', payload)
 }
 
+export function addAliyunKey(payload) {
+  return post(devopsApi.getAliyunKeyList, payload)
+}
+
 export function getAliyunKeyList() {
   return get(devopsApi.getAliyunKeyList)
 }
@@ -226,7 +230,7 @@ export function getArgList(queryInfo) {
 }
 
 export function addCompiler(formData) {
-  return postForm(devopsApi.getCompilerList, formData)
+  return post(devopsApi.getCompilerList, formData)
 }
 
 export function deleteCompiler(formData) {
@@ -242,7 +246,7 @@ export function getPackTypes() {
 }
 
 export function addPacker(formData) {
-  return postForm(devopsApi.getPackerList, formData)
+  return post(devopsApi.getPackerList, formData)
 }
 
 export function deletePacker(formData) {

+ 181 - 0
src/components/card/AliyunAddCard.vue

@@ -0,0 +1,181 @@
+<template>
+  <div>
+    <el-form ref="form" :model="form" label-width="100px">
+      <el-form-item label="仓库认证" prop="repoAuthName">
+        <el-row :gutter="10">
+          <el-col :span="14">
+            <el-select
+              v-model="form.repoAuthName"
+              placeholder="请选择仓库认证"
+              style="width: 100%"
+              :disabled="repoAuthNames.length === 0"
+            >
+              <el-option
+                v-for="(item, index) in repoAuthNames"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-col>
+          <el-col :span="6">
+            <el-button
+              type="text"
+              icon="el-icon-plus"
+              @click="handleAddRepoAuth"
+            >
+              {{ repoAuthNames.length === 0 ? '暂无数据,去添加' : '新增认证' }}
+            </el-button>
+          </el-col>
+        </el-row>
+      </el-form-item>
+      <el-form-item label="OSS 地址">
+        <el-input v-model="form.endpoint" style="width: 70%" placeholder="e.g. registry.cn-hangzhou.aliyuncs.com" />
+      </el-form-item>
+      <el-form-item label="OSS 名字">
+        <el-input v-model="form.name" style="width: 70%" placeholder="e.g. reghao/" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onAddOss">确定</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-dialog
+      title="添加仓库认证"
+      :visible.sync="showAddRepoAuthDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <repo-auth-add-card
+        @close="showAddRepoAuthDialog = false"
+        @success="handleAddRepoAuthSuccess"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import RepoAuthAddCard from "@/components/card/RepoAuthAddCard.vue";
+import {addAliyunKey, getRepoAuthNames} from "@/api/devops";
+
+export default {
+  name: 'AliyunAddCard',
+  components: {RepoAuthAddCard},
+  data() {
+    return {
+      loading: false,
+      active: 0,
+      repoAuthNames: [],
+      form: {
+        repoAuthName: '',
+        endpoint: '',
+        name: ''
+      },
+      showAddRepoAuthDialog: false
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    // 1. 初始化数据:从后端获取下拉框选项
+    async initData() {
+      this.loading = true
+      try {
+        // 这里替换为你真实的 API 调用
+        const [resp] = await Promise.all([getRepoAuthNames()])
+        this.repoAuthNames = resp.data
+      } catch (error) {
+        this.$message.error('获取配置选项失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    // 异步校验并跳转
+    async nextStep() {
+      const stepFields = ['type', 'authType', 'name'] // 第一步需要校验的字段
+
+      try {
+        // 关键:将所有字段的校验转为 Promise 数组
+        const checkActions = stepFields.map(field => {
+          return new Promise((resolve, reject) => {
+            this.$refs.form.validateField(field, error => {
+              if (error) reject(error)
+              else resolve()
+            })
+          })
+        })
+
+        await Promise.all(checkActions)
+        this.active++ // 只有全部校验成功才加 1
+      } catch (e) {
+        this.$message.warning('请检查基础配置是否填写完整')
+      }
+    },
+    async onAddOss() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          addAliyunKey(this.form).then(resp => {
+            this.$message.info(resp.msg)
+          }).catch(error => {
+            this.$message.error(error.message)
+          }).finally(() => {
+          })
+          /*addRepoAuth(this.form).then(resp => {
+            this.$message.info(resp.msg)
+            this.$refs.form.resetFields();
+            this.$emit('success', resp)
+          }).catch(error => {
+            this.$emit('fail', error)
+          }).finally(() => {
+          })*/
+        } else {
+          return false
+        }
+      })
+    },
+    async handleAddRepoAuth() {
+      this.showAddRepoAuthDialog = true
+    },
+    handleAddRepoAuthSuccess() {
+      this.showAddRepoAuthDialog = false
+      getRepoAuthNames().then(resp => {
+        if (resp.code === 0) {
+          this.repoAuthNames = resp.data
+        } else {
+          this.$message.info(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.repo-auth-container {
+  padding: 10px 20px;
+}
+.step-content {
+  min-height: 240px;
+  margin-top: 20px;
+}
+.dialog-footer {
+  text-align: right;
+  padding-top: 20px;
+  margin-top: 20px;
+  border-top: 1px solid #f2f6fc;
+}
+.none-tip {
+  padding-top: 40px;
+}
+
+::v-deep .el-textarea__inner {
+  font-family: 'Fira Code', 'Courier New', monospace;
+  background-color: #fcfcfc;
+  font-size: 13px;
+}
+</style>

+ 185 - 0
src/components/card/CompilerAddCard.vue

@@ -0,0 +1,185 @@
+<template>
+  <div class="compiler-config-steps">
+    <el-steps :active="active" finish-status="success" simple style="margin-bottom: 30px">
+      <el-step title="配置基础环境" icon="el-icon-monitor" />
+      <el-step title="编写编译脚本" icon="el-icon-edit" />
+    </el-steps>
+
+    <el-form ref="form" :model="form" :rules="dynamicRules" label-width="130px">
+      <div v-show="active === 0">
+        <el-form-item label="编译类型" prop="type">
+          <el-select v-model="form.type" placeholder="请选择编译类型" style="width: 100%">
+            <el-option
+              v-for="(item, index) in compileTypes"
+              :key="index"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="配置名称" prop="name">
+          <el-input v-model="form.name" placeholder="例如 mvn-build" />
+        </el-form-item>
+      </div>
+
+      <div v-show="active === 1">
+        <el-form-item v-if="form.type === 'dockerBuild'" label="提示">
+          <el-input value="dockerBuild 类型可能还需要设置 dockerfile 中的参数变量" readonly />
+        </el-form-item>
+        <el-form-item v-if="form.type === 'dockerRun'" label="编译镜像" prop="compilerImage">
+          <el-input v-model="form.compilerImage" placeholder="例如 amazoncorretto:17.0.16-al2-native-jdk" />
+          <div class="form-tip">该镜像将作为构建时的 Runtime 容器</div>
+          <div class="form-tip">dockerRun 类型可能还需要设置容器的环境变量和映射目录</div>
+        </el-form-item>
+        <el-form-item v-if="form.type !== 'dockerBuild'" label="编译命令" prop="compileCmd">
+          <el-input
+            v-model="form.compileCmd"
+            type="textarea"
+            :autosize="{ minRows: 4 }"
+            placeholder="例如 mvn clean package -Dmaven.test.skip"
+          />
+          <div class="form-tip">shell 类型需要使用绝对路径</div>
+          <div class="form-tip">dockerRun 类型直接使用相应命令即可</div>
+        </el-form-item>
+        <el-form-item v-if="form.type !== 'dockerBuild'" label="编译器版本命令" prop="versionCmd">
+          <el-input v-model="form.versionCmd" placeholder="例如 mvn -v" />
+          <div class="form-tip">shell 类型需要使用绝对路径</div>
+          <div class="form-tip">dockerRun 类型直接使用相应命令即可</div>
+        </el-form-item>
+      </div>
+
+      <el-form-item style="margin-top: 40px">
+        <el-button v-if="active > 0" @click="active--">上一步</el-button>
+        <el-button v-if="active < 1" type="primary" @click="nextStep">下一步</el-button>
+        <el-button v-if="active === 1" type="success" @click="onAddCompiler">提交保存</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import {addCompiler, getCompilerTypes } from "@/api/devops";
+
+export default {
+  name: 'CompilerAddCard',
+  data() {
+    return {
+      loading: false,
+      active: 0,
+      form: {
+        type: '',
+        name: '',
+        compileCmd: '',
+        versionCmd: '',
+        compilerImage: ''
+      },
+      compileTypes: []
+    }
+  },
+  computed: {
+    dynamicRules() {
+      const baseRules = {
+        type: [{ required: true, message: '请选择类型', trigger: 'blur' }],
+        name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
+      }
+      // 根据选择的类型增加特定规则
+      if (this.form.type === 'dockerRun') {
+        baseRules.compilerImage = [{ required: true, message: 'dockerRun 类型必须指定编译使用的镜像', trigger: 'blur' }]
+      }
+      if (this.form.type === 'shell' || this.form.type === 'dockerRun') {
+        baseRules.compileCmd = [{ required: true, message: 'shell 和 dockerRun 类型必须填写编译命令', trigger: 'blur' }]
+        baseRules.versionCmd = [{ required: true, message: 'shell 和 dockerRun 类型必须填写编译器的版本命令', trigger: 'blur' }]
+      }
+      return baseRules
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    // 1. 初始化数据:从后端获取下拉框选项
+    async initData() {
+      this.loading = true
+      try {
+        // 这里替换为你真实的 API 调用
+        const [resp] = await Promise.all([getCompilerTypes()])
+        this.compileTypes = resp.data
+      } catch (error) {
+        this.$message.error('获取配置选项失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    // 异步校验并跳转
+    async nextStep() {
+      const stepFields = ['type', 'name'] // 第一步需要校验的字段
+
+      try {
+        // 关键:将所有字段的校验转为 Promise 数组
+        const checkActions = stepFields.map(field => {
+          return new Promise((resolve, reject) => {
+            this.$refs.form.validateField(field, error => {
+              if (error) reject(error)
+              else resolve()
+            })
+          })
+        })
+
+        await Promise.all(checkActions)
+        this.active++ // 只有全部校验成功才加 1
+      } catch (e) {
+        this.$message.warning('请检查基础配置是否填写完整')
+      }
+    },
+    onAddCompiler() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          addCompiler(this.form).then(resp => {
+            this.$message.info(resp.msg)
+            // this.$refs.form.resetFields();
+            this.$emit('success', resp)
+          }).catch(error => {
+            this.$message.error(error.message)
+            this.$emit('fail', error)
+          }).finally(() => {
+          })
+        } else {
+          return false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.compiler-config-steps {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  background: #fff;
+  border-radius: 8px;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 2;
+}
+
+/* 使用 ::v-deep 穿透 scoped 限制
+  这是 Vue 2 中兼容性最好的写法,支持 Less/Sass
+*/
+::v-deep .el-textarea__inner {
+  font-family: 'Courier New', Courier, monospace;
+  background-color: #f8f9fa;
+  color: #2c3e50;
+  border: 1px solid #dcdfe6;
+}
+
+/* 鼠标悬停和聚焦时的效果优化 */
+::v-deep .el-textarea__inner:focus {
+  background-color: #fff;
+  border-color: #409EFF;
+}
+</style>

+ 319 - 0
src/components/card/PackerAddCard.vue

@@ -0,0 +1,319 @@
+<template>
+  <div>
+    <div class="compiler-config-steps">
+      <el-steps :active="active" finish-status="success" simple style="margin-bottom: 30px">
+        <el-step title="配置基础环境" icon="el-icon-monitor" />
+        <el-step title="配置打包" icon="el-icon-edit" />
+      </el-steps>
+
+      <el-form ref="form" :model="form" :rules="dynamicRules" label-width="130px">
+        <div v-show="active === 0">
+          <el-form-item label="打包类型" prop="type">
+            <el-select v-model="form.type" placeholder="请选择打包类型" style="width: 100%">
+              <el-option
+                v-for="(item, index) in packTypes"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="配置名称" prop="name">
+            <el-input v-model="form.name" placeholder="例如 docker-pack" />
+          </el-form-item>
+        </div>
+
+        <div v-show="active === 1">
+          <div v-if="form.type === 'docker'">
+            <el-form-item label="docker 仓库" prop="dockerRegistry">
+              <div style="display: flex; align-items: center;">
+                <el-select v-model="form.dockerRegistry" placeholder="请选择 docker 仓库" style="width: 100%">
+                  <el-option
+                    v-for="(item, index) in registryList"
+                    :key="index"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+                <el-tooltip content="添加 docker 仓库" placement="top">
+                  <el-button
+                    type="primary"
+                    icon="el-icon-plus"
+                    circle
+                    size="mini"
+                    style="margin-left: 10px"
+                    @click="goToAddRegistry"
+                  />
+                </el-tooltip>
+              </div>
+              <div class="form-tip">编译构建生成的镜像会推送到该仓库</div>
+            </el-form-item>
+          </div>
+          <div v-else-if="form.type === 'ossStatic'" class="oss-config-group">
+            <el-form-item prop="artifactPath">
+                  <span slot="label">
+                    编译产物路径
+                    <el-tooltip content="编译完成后产物所在的相对路径,例如:dist/ 或 target/app.jar" placement="top">
+                      <i class="el-icon-question" style="cursor: help; color: #909399;"></i>
+                    </el-tooltip>
+                  </span>
+              <el-input v-model="form.artifactPath" placeholder="例如 dist/" clearable />
+              <div class="form-tip">源码根目录下的相对路径</div>
+            </el-form-item>
+            <el-form-item label="阿里云 OSS" prop="ossEndpoint">
+              <div class="oss-select-wrapper">
+                <el-select
+                  v-model="form.ossEndpoint"
+                  placeholder="请选择阿里云 OSS"
+                  style="flex: 1"
+                  filterable
+                >
+                  <el-option
+                    v-for="(item, index) in ossList"
+                    :key="index"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+
+                <el-tooltip content="添加新的 OSS" placement="top">
+                  <el-button
+                    type="primary"
+                    icon="el-icon-plus"
+                    circle
+                    size="small"
+                    class="add-bucket-btn"
+                    @click="goToAddOss"
+                  />
+                </el-tooltip>
+              </div>
+              <div class="form-tip">编译生成的静态产物将自动上传并同步至该 OSS, 应用部署配置中会指定使用的 bucket</div>
+            </el-form-item>
+          </div>
+          <div v-else>
+            <el-form-item prop="artifactPath">
+                  <span slot="label">
+                    编译产物路径
+                    <el-tooltip content="编译完成后产物所在的相对路径,例如:dist/ 或 target/app.jar" placement="top">
+                      <i class="el-icon-question" style="cursor: help; color: #909399;"></i>
+                    </el-tooltip>
+                  </span>
+              <el-input v-model="form.artifactPath" placeholder="例如 dist/" clearable />
+              <div class="form-tip">源码根目录下的相对路径</div>
+            </el-form-item>
+          </div>
+        </div>
+
+        <el-form-item style="margin-top: 40px">
+          <el-button v-if="active > 0" @click="active--">上一步</el-button>
+          <el-button v-if="active < 1" type="primary" @click="nextStep">下一步</el-button>
+          <el-button v-if="active === 1" type="success" @click="onAddPacker">提交保存</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <el-dialog
+      title="添加 docker 仓库"
+      :visible.sync="showAddRegistryDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <registry-add-card
+        @close="showAddRegistryDialog = false"
+        @success="onAddSuccess"
+      />
+    </el-dialog>
+    <el-dialog
+      title="添加阿里云 OSS"
+      :visible.sync="showAddOssDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <aliyun-add-card
+        @close="showAddOssDialog = false"
+        @success="onAddSuccess"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import RegistryAddCard from '@/components/card/RegistryAddCard.vue'
+import AliyunAddCard from '@/components/card/AliyunAddCard.vue'
+import { addPacker, getPackTypes } from "@/api/devops";
+
+export default {
+  name: 'PackerAddCard',
+  components: {
+    RegistryAddCard,
+    AliyunAddCard
+  },
+  data() {
+    return {
+      loading: false,
+      active: 0,
+      form: {
+        type: '',
+        name: '',
+        artifactPath: '',
+        dockerRegistry: '',
+        ossEndpoint: '',
+        targetPath: ''
+      },
+      packTypes: [],
+      registryList: [],
+      ossList: [],
+      // **********************************************************************
+      showAddRegistryDialog: false,
+      showAddOssDialog: false
+    }
+  },
+  computed: {
+    dynamicRules() {
+      const baseRules = {
+        type: [{ required: true, message: '请选择仓库类型', trigger: 'blur' }],
+        name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+        authType: [{ required: true, message: '请选择认证类型', trigger: 'blur' }]
+      }
+      // 根据选择的类型增加特定规则
+      if (this.form.authType === 'ssh') {
+        baseRules.rsaPrikey = [{ required: true, message: 'ssh 认证类型必须填写 RSA 私钥', trigger: 'blur' }]
+        baseRules.rsaPubkey = [{ required: true, message: 'ssh 认证类型必须填写 RSA 公钥', trigger: 'blur' }]
+      } else if (this.form.authType === 'http') {
+        baseRules.username = [{ required: true, message: 'http 认证类型必须填写 username', trigger: 'blur' }]
+        baseRules.password = [{ required: true, message: 'http 认证类型必须填写 password', trigger: 'blur' }]
+      }
+      return baseRules
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    // 1. 初始化数据:从后端获取下拉框选项
+    async initData() {
+      this.loading = true
+      try {
+        // 这里替换为你真实的 API 调用
+        const [resp] = await Promise.all([getPackTypes()])
+        this.packTypes = resp.data.packTypes
+        this.registryList = resp.data.registryList
+        this.ossList = resp.data.ossList
+      } catch (error) {
+        this.$message.error('获取配置选项失败 ' + error.message)
+      } finally {
+        this.loading = false
+      }
+    },
+    // 异步校验并跳转
+    async nextStep() {
+      const stepFields = ['type', 'name'] // 第一步需要校验的字段
+
+      try {
+        // 关键:将所有字段的校验转为 Promise 数组
+        const checkActions = stepFields.map(field => {
+          return new Promise((resolve, reject) => {
+            this.$refs.form.validateField(field, error => {
+              if (error) reject(error)
+              else resolve()
+            })
+          })
+        })
+
+        await Promise.all(checkActions)
+        this.active++ // 只有全部校验成功才加 1
+      } catch (e) {
+        this.$message.warning('请检查基础配置是否填写完整')
+      }
+    },
+    onAddPacker() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          addPacker(this.form).then(resp => {
+            this.$message.info(resp.msg)
+            // this.$refs.form.resetFields();
+            this.$emit('success', resp)
+          }).catch(error => {
+            this.$message.error(error.message)
+            this.$emit('fail', error)
+          }).finally(() => {
+          })
+        } else {
+          return false
+        }
+      })
+    },
+    goToAddRegistry() {
+      this.showAddRegistryDialog = true
+    },
+    goToAddOss() {
+      this.showAddOssDialog = true
+    },
+    onAddSuccess() {
+      getPackTypes().then(resp => {
+        if (resp.code === 0) {
+          this.packTypes = resp.data.packTypes
+          this.registryList = resp.data.registryList
+          this.ossList = resp.data.ossList
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.compiler-config-steps {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  background: #fff;
+  border-radius: 8px;
+}
+
+/* OSS 配置组容器,可以加一个淡色背景或边框区分 */
+.oss-config-group {
+  padding: 15px;
+  background-color: #fcfcfc;
+  border-radius: 4px;
+  border: 1px dashed #e4e7ed;
+  margin-bottom: 20px;
+}
+
+/* Flex 布局让选择框和按钮完美对齐 */
+.oss-select-wrapper {
+  display: flex;
+  align-items: center;
+  gap: 12px; /* 间距更加现代化 */
+}
+
+/* 按钮悬停动画 */
+.add-bucket-btn {
+  transition: transform 0.2s;
+}
+.add-bucket-btn:hover {
+  transform: scale(1.1);
+}
+
+/* 提示文字样式优化 */
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+  margin-top: 4px;
+}
+
+/* 统一图标颜色 */
+.el-icon-question {
+  font-size: 14px;
+  vertical-align: middle;
+}
+</style>

+ 181 - 0
src/components/card/RegistryAddCard.vue

@@ -0,0 +1,181 @@
+<template>
+  <div>
+    <el-form ref="form" :model="form" label-width="100px">
+      <el-form-item label="仓库认证" prop="repoAuthName">
+        <el-row :gutter="10">
+          <el-col :span="14">
+            <el-select
+              v-model="form.repoAuthName"
+              placeholder="请选择仓库认证"
+              style="width: 100%"
+              :disabled="repoAuthNames.length === 0"
+            >
+              <el-option
+                v-for="(item, index) in repoAuthNames"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-col>
+          <el-col :span="6">
+            <el-button
+              type="text"
+              icon="el-icon-plus"
+              @click="handleAddRepoAuth"
+            >
+              {{ repoAuthNames.length === 0 ? '暂无数据,去添加' : '新增认证' }}
+            </el-button>
+          </el-col>
+        </el-row>
+      </el-form-item>
+      <el-form-item label="仓库地址">
+        <el-input v-model="form.registryUrl" style="width: 70%" placeholder="e.g. registry.cn-hangzhou.aliyuncs.com" />
+      </el-form-item>
+      <el-form-item label="命名空间">
+        <el-input v-model="form.registryNamespace" style="width: 70%" placeholder="e.g. reghao/" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onAddRegistry">确定</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-dialog
+      title="添加仓库认证"
+      :visible.sync="showAddRepoAuthDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <repo-auth-add-card
+        @close="showAddRepoAuthDialog = false"
+        @success="handleAddRepoAuthSuccess"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import RepoAuthAddCard from "@/components/card/RepoAuthAddCard.vue";
+import { addDockerRegistry, getRepoAuthNames } from "@/api/devops";
+
+export default {
+  name: 'RegistryAddCard',
+  components: {RepoAuthAddCard},
+  data() {
+    return {
+      loading: false,
+      active: 0,
+      repoAuthNames: [],
+      form: {
+        repoAuthName: '',
+        registryUrl: '',
+        registryNamespace: ''
+      },
+      showAddRepoAuthDialog: false
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    // 1. 初始化数据:从后端获取下拉框选项
+    async initData() {
+      this.loading = true
+      try {
+        // 这里替换为你真实的 API 调用
+        const [resp] = await Promise.all([getRepoAuthNames()])
+        this.repoAuthNames = resp.data
+      } catch (error) {
+        this.$message.error('获取配置选项失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    // 异步校验并跳转
+    async nextStep() {
+      const stepFields = ['type', 'authType', 'name'] // 第一步需要校验的字段
+
+      try {
+        // 关键:将所有字段的校验转为 Promise 数组
+        const checkActions = stepFields.map(field => {
+          return new Promise((resolve, reject) => {
+            this.$refs.form.validateField(field, error => {
+              if (error) reject(error)
+              else resolve()
+            })
+          })
+        })
+
+        await Promise.all(checkActions)
+        this.active++ // 只有全部校验成功才加 1
+      } catch (e) {
+        this.$message.warning('请检查基础配置是否填写完整')
+      }
+    },
+    async onAddRegistry() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          addDockerRegistry(this.form).then(resp => {
+            this.$message.info(resp.msg)
+          }).catch(error => {
+            this.$message.error(error.message)
+          }).finally(() => {
+          })
+          /*addRepoAuth(this.form).then(resp => {
+            this.$message.info(resp.msg)
+            this.$refs.form.resetFields();
+            this.$emit('success', resp)
+          }).catch(error => {
+            this.$emit('fail', error)
+          }).finally(() => {
+          })*/
+        } else {
+          return false
+        }
+      })
+    },
+    async handleAddRepoAuth() {
+      this.showAddRepoAuthDialog = true
+    },
+    handleAddRepoAuthSuccess() {
+      this.showAddRepoAuthDialog = false
+      getRepoAuthNames().then(resp => {
+        if (resp.code === 0) {
+          this.repoAuthNames = resp.data
+        } else {
+          this.$message.info(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.repo-auth-container {
+  padding: 10px 20px;
+}
+.step-content {
+  min-height: 240px;
+  margin-top: 20px;
+}
+.dialog-footer {
+  text-align: right;
+  padding-top: 20px;
+  margin-top: 20px;
+  border-top: 1px solid #f2f6fc;
+}
+.none-tip {
+  padding-top: 40px;
+}
+
+::v-deep .el-textarea__inner {
+  font-family: 'Fira Code', 'Courier New', monospace;
+  background-color: #fcfcfc;
+  font-size: 13px;
+}
+</style>

+ 201 - 0
src/components/card/RepoAuthAddCard.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="repo-auth-container" v-loading="loading">
+    <div class="compiler-config-steps">
+      <el-steps :active="active" finish-status="success" simple style="margin-bottom: 30px">
+        <el-step title="基础配置" icon="el-icon-monitor" />
+        <el-step title="认证配置" icon="el-icon-edit" />
+      </el-steps>
+
+      <el-form ref="form" :model="form" :rules="dynamicRules" label-width="130px">
+        <div v-show="active === 0">
+          <el-form-item label="认证类型" prop="authType">
+            <el-select v-model="form.authType" placeholder="请选择认证类型" style="width: 100%">
+              <el-option
+                v-for="(item, index) in authTypes"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="认证名称" prop="name">
+            <el-input v-model="form.name" placeholder="例如 git-auth" />
+          </el-form-item>
+          <el-form-item label="仓库类型" prop="type">
+            <el-select v-model="form.type" placeholder="请选择仓库类型" style="width: 100%">
+              <el-option
+                v-for="(item, index) in repoTypes"
+                :key="index"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+        </div>
+
+        <div v-show="active === 1">
+          <div v-if="form.authType === 'ssh'">
+            <el-form-item label="RSA 私钥" prop="rsaPrikey">
+              <el-input
+                v-model="form.rsaPrikey"
+                type="textarea"
+                :autosize="{ minRows: 4 }"
+                placeholder="请填写 RSA 私钥"
+              />
+            </el-form-item>
+            <el-form-item label="RSA 公钥" prop="rsaPubkey">
+              <el-input
+                v-model="form.rsaPubkey"
+                type="textarea"
+                :autosize="{ minRows: 4 }"
+                placeholder="请填写 RSA 公钥"
+              />
+            </el-form-item>
+          </div>
+          <div v-else-if="form.authType === 'http'">
+            <el-form-item label="用户名" prop="username">
+              <el-input v-model="form.username" placeholder="请填写用户名" />
+            </el-form-item>
+            <el-form-item label="密码" prop="password">
+              <el-input v-model="form.password" placeholder="请填写密码" />
+            </el-form-item>
+          </div>
+          <div v-else>
+            <el-input value="none 认证类型不需要填写任何认证资料" readonly />
+          </div>
+        </div>
+
+        <el-form-item style="margin-top: 40px">
+          <el-button v-if="active > 0" @click="active--">上一步</el-button>
+          <el-button v-if="active < 1" type="primary" @click="nextStep">下一步</el-button>
+          <el-button v-if="active === 1" type="success" @click="onAddRepoAuth">提交保存</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { addRepoAuth, getRepoTypes } from "@/api/devops";
+
+export default {
+  name: 'RepoAuthAddCard',
+  data() {
+    return {
+      loading: false,
+      active: 0,
+      repoTypes: [],
+      authTypes: [],
+      form: {
+        authType: 'http',
+        name: '',
+        type: 'git',
+        username: '',
+        password: '',
+        rsaPrikey: '',
+        rsaPubkey: '',
+      }
+    }
+  },
+  computed: {
+    dynamicRules() {
+      const baseRules = {
+        type: [{ required: true, message: '请选择仓库类型', trigger: 'blur' }],
+        name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+        authType: [{ required: true, message: '请选择认证类型', trigger: 'blur' }]
+      }
+      // 根据选择的类型增加特定规则
+      if (this.form.authType === 'ssh') {
+        baseRules.rsaPrikey = [{ required: true, message: 'ssh 认证类型必须填写 RSA 私钥', trigger: 'blur' }]
+        baseRules.rsaPubkey = [{ required: true, message: 'ssh 认证类型必须填写 RSA 公钥', trigger: 'blur' }]
+      } else if (this.form.authType === 'http') {
+        baseRules.username = [{ required: true, message: 'http 认证类型必须填写 username', trigger: 'blur' }]
+        baseRules.password = [{ required: true, message: 'http 认证类型必须填写 password', trigger: 'blur' }]
+      }
+      return baseRules
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    // 1. 初始化数据:从后端获取下拉框选项
+    async initData() {
+      this.loading = true
+      try {
+        // 这里替换为你真实的 API 调用
+        const [resp] = await Promise.all([getRepoTypes()])
+        this.repoTypes = resp.data.repoTypes
+        this.authTypes = resp.data.authTypes
+      } catch (error) {
+        this.$message.error('获取配置选项失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    // 异步校验并跳转
+    async nextStep() {
+      const stepFields = ['type', 'authType', 'name'] // 第一步需要校验的字段
+
+      try {
+        // 关键:将所有字段的校验转为 Promise 数组
+        const checkActions = stepFields.map(field => {
+          return new Promise((resolve, reject) => {
+            this.$refs.form.validateField(field, error => {
+              if (error) reject(error)
+              else resolve()
+            })
+          })
+        })
+
+        await Promise.all(checkActions)
+        this.active++ // 只有全部校验成功才加 1
+      } catch (e) {
+        this.$message.warning('请检查基础配置是否填写完整')
+      }
+    },
+    async onAddRepoAuth() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          addRepoAuth(this.form).then(resp => {
+            this.$message.info(resp.msg)
+            this.$refs.form.resetFields();
+            this.$emit('success', resp)
+          }).catch(error => {
+            this.$message.error(error.message)
+            this.$emit('fail', error)
+          }).finally(() => {
+          })
+        } else {
+          return false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.repo-auth-container {
+  padding: 10px 20px;
+}
+.step-content {
+  min-height: 240px;
+  margin-top: 20px;
+}
+.dialog-footer {
+  text-align: right;
+  padding-top: 20px;
+  margin-top: 20px;
+  border-top: 1px solid #f2f6fc;
+}
+.none-tip {
+  padding-top: 40px;
+}
+
+::v-deep .el-textarea__inner {
+  font-family: 'Fira Code', 'Courier New', monospace;
+  background-color: #fcfcfc;
+  font-size: 13px;
+}
+</style>

+ 126 - 46
src/views/devops/app/AppConfig.vue

@@ -312,7 +312,7 @@
                 <div style="display: flex; align-items: center;">
                   <el-select
                       v-model="appForm.repoAuthConfigId"
-                      placeholder="选择认证"
+                      placeholder="选择仓库认证"
                       style="flex: 1; margin-right: 10px;"
                       clearable
                   >
@@ -330,7 +330,6 @@
                       </el-button>
                     </div>
                   </el-select>
-
                   <el-button
                       type="primary"
                       icon="el-icon-plus"
@@ -355,14 +354,68 @@
                 </el-radio-group>
               </el-form-item>
               <el-form-item label="编译器配置" prop="compilerConfigId">
-                <el-select v-model="appForm.compilerConfigId">
-                  <el-option v-for="item in compilerList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                </el-select>
+                <div style="display: flex; align-items: center;">
+                  <el-select
+                    v-model="appForm.compilerConfigId"
+                    placeholder="选择编译配置"
+                    style="flex: 1; margin-right: 10px;"
+                    clearable
+                  >
+                    <el-option
+                      v-for="item in compilerList"
+                      :key="item.value"
+                      :label="item.label"
+                      :value="item.value"
+                    ></el-option>
+
+                    <div slot="empty" style="text-align: center; padding: 10px 0;">
+                      <p style="margin-bottom: 10px; color: #909399; font-size: 12px;">暂无编译配置</p>
+                      <el-button type="text" size="mini" icon="el-icon-plus" @click="showAddCompilerDialog = true">
+                        立即添加
+                      </el-button>
+                    </div>
+                  </el-select>
+                  <el-button
+                    type="primary"
+                    icon="el-icon-plus"
+                    circle
+                    size="mini"
+                    @click="showAddCompilerDialog = true"
+                    title="添加新编译配置"
+                  ></el-button>
+                </div>
               </el-form-item>
               <el-form-item label="打包配置" prop="packerConfigId">
-                <el-select v-model="appForm.packerConfigId">
-                  <el-option v-for="item in packerList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                </el-select>
+                <div style="display: flex; align-items: center;">
+                  <el-select
+                    v-model="appForm.packerConfigId"
+                    placeholder="选择打包配置"
+                    style="flex: 1; margin-right: 10px;"
+                    clearable
+                  >
+                    <el-option
+                      v-for="item in packerList"
+                      :key="item.value"
+                      :label="item.label"
+                      :value="item.value"
+                    ></el-option>
+
+                    <div slot="empty" style="text-align: center; padding: 10px 0;">
+                      <p style="margin-bottom: 10px; color: #909399; font-size: 12px;">暂无打包配置</p>
+                      <el-button type="text" size="mini" icon="el-icon-plus" @click="showAddPackerDialog = true">
+                        立即添加
+                      </el-button>
+                    </div>
+                  </el-select>
+                  <el-button
+                    type="primary"
+                    icon="el-icon-plus"
+                    circle
+                    size="mini"
+                    @click="showAddPackerDialog = true"
+                    title="添加新打包配置"
+                  ></el-button>
+                </div>
               </el-form-item>
             </div>
 
@@ -492,24 +545,53 @@
         </el-form>
       </template>
     </el-dialog>
-    <el-dialog title="添加仓库认证" :visible.sync="showAddRepoAuthDialog" width="400px" append-to-body>
-      <el-form :model="repoAuthForm" label-width="80px">
-        <el-form-item label="认证名称">
-          <el-input v-model="repoAuthForm.label" placeholder="如:GitLab-SSH"></el-input>
-        </el-form-item>
-        <el-form-item label="私钥/密码">
-          <el-input v-model="repoAuthForm.value" type="password"></el-input>
-        </el-form-item>
-      </el-form>
-      <div slot="footer">
-        <el-button @click="showAddRepoAuthDialog = false">取消</el-button>
-        <el-button type="primary" @click="handleAddRepoAuth">确定</el-button>
-      </div>
+    <el-dialog
+      title="添加仓库认证"
+      :visible.sync="showAddRepoAuthDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <repo-auth-add-card
+        @close="showAddRepoAuthDialog = false"
+        @success="handleAddRepoAuthSuccess"
+      />
+    </el-dialog>
+    <el-dialog
+      title="添加编译配置"
+      :visible.sync="showAddCompilerDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <compiler-add-card
+        @close="showAddCompilerDialog = false"
+        @success="handleAddCompilerSuccess"
+      />
+    </el-dialog>
+    <el-dialog
+      title="添加打包配置"
+      :visible.sync="showAddPackerDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <packer-add-card
+        @close="showAddPackerDialog = false"
+        @success="handleAddPackerSuccess"
+      />
     </el-dialog>
   </el-container>
 </template>
 
 <script>
+import RepoAuthAddCard from '@/components/card/RepoAuthAddCard.vue'
+import CompilerAddCard from '@/components/card/CompilerAddCard.vue'
+import PackerAddCard from '@/components/card/PackerAddCard.vue'
+
 import {
   addAppBindDomain,
   addAppConfig, addAppDeployConfig,
@@ -527,6 +609,11 @@ import {
 
 export default {
   name: 'AppConfig',
+  components: {
+    RepoAuthAddCard,
+    CompilerAddCard,
+    PackerAddCard
+  },
   data() {
     return {
       envList: [],
@@ -566,11 +653,13 @@ export default {
         dockerfile: [{ required: true, message: '请填写 Dockerfile', trigger: 'blur' }],
       },
       // **********************************************************************
-      showAddRepoAuthDialog: false,
       repoAuthForm: {
         appId: '',
         domain: ''
       },
+      showAddRepoAuthDialog: false,
+      showAddCompilerDialog: false,
+      showAddPackerDialog: false,
       // **********************************************************************
       showBindDomainDialog: false,
       domainList: [],
@@ -759,28 +848,7 @@ export default {
       })
     },
     handleEdit(index, row) {
-      getBuildConfig().then(resp => {
-        if (resp.code === 0) {
-          this.repoAuthList = resp.data.repoAuthList
-          this.compilerList = resp.data.compilerList
-          this.packerList = resp.data.packerList
-
-          getAppConfig(row.appId).then(resp => {
-            if (resp.code === 0) {
-              this.editForm = resp.data
-              this.showEditDialog = true
-            } else {
-              this.$message.error(resp.msg)
-            }
-          }).catch(error => {
-            this.$message.error(error.message)
-          })
-        } else {
-          this.$message.error(resp.msg)
-        }
-      }).catch(error => {
-        this.$message.error(error.message)
-      })
+      this.getBuildConfigWrapper()
     },
     onEdit() {
       const formData = new FormData()
@@ -815,6 +883,9 @@ export default {
       })
     },
     handleAdd() {
+      this.getBuildConfigWrapper()
+    },
+    getBuildConfigWrapper() {
       getBuildConfig().then(resp => {
         if (resp.code === 0) {
           this.repoAuthList = resp.data.repoAuthList
@@ -1017,8 +1088,17 @@ export default {
         })
       })
     },
-    handleAddRepoAuth() {
-      this.$message.info('add repoauth')
+    handleAddRepoAuthSuccess() {
+      this.showAddRepoAuthDialog = false
+      this.getBuildConfigWrapper()
+    },
+    handleAddCompilerSuccess() {
+      this.showAddCompilerDialog = false
+      this.getBuildConfigWrapper()
+    },
+    handleAddPackerSuccess() {
+      this.showAddPackerDialog = false
+      this.getBuildConfigWrapper()
     }
   }
 }

+ 14 - 175
src/views/devops/build/Compiler.vue

@@ -115,64 +115,10 @@
       :visible.sync="showAddDialog"
       center
     >
-      <template>
-        <div class="compiler-config-steps">
-          <el-steps :active="active" finish-status="success" simple style="margin-bottom: 30px">
-            <el-step title="配置基础环境" icon="el-icon-monitor" />
-            <el-step title="编写编译脚本" icon="el-icon-edit" />
-          </el-steps>
-
-          <el-form ref="form" :model="form" :rules="dynamicRules" label-width="130px">
-            <div v-show="active === 0">
-              <el-form-item label="编译类型" prop="type">
-                <el-select v-model="form.type" placeholder="请选择编译类型" style="width: 100%">
-                  <el-option
-                    v-for="(item, index) in compileTypes"
-                    :key="index"
-                    :label="item.label"
-                    :value="item.value"
-                  />
-                </el-select>
-              </el-form-item>
-              <el-form-item label="配置名称" prop="name">
-                <el-input v-model="form.name" placeholder="例如 mvn-build" />
-              </el-form-item>
-            </div>
-
-            <div v-show="active === 1">
-              <el-form-item v-if="form.type === 'dockerBuild'" label="提示">
-                <el-input value="dockerBuild 类型可能还需要设置 dockerfile 中的参数变量" readonly />
-              </el-form-item>
-              <el-form-item v-if="form.type === 'dockerRun'" label="编译镜像" prop="compilerImage">
-                <el-input v-model="form.compilerImage" placeholder="例如 amazoncorretto:17.0.16-al2-native-jdk" />
-                <div class="form-tip">该镜像将作为构建时的 Runtime 容器</div>
-                <div class="form-tip">dockerRun 类型可能还需要设置容器的环境变量和映射目录</div>
-              </el-form-item>
-              <el-form-item v-if="form.type !== 'dockerBuild'" label="编译命令" prop="compileCmd">
-                <el-input
-                  v-model="form.compileCmd"
-                  type="textarea"
-                  :autosize="{ minRows: 4 }"
-                  placeholder="例如 mvn clean package -Dmaven.test.skip"
-                />
-                <div class="form-tip">shell 类型需要使用绝对路径</div>
-                <div class="form-tip">dockerRun 类型直接使用相应命令即可</div>
-              </el-form-item>
-              <el-form-item v-if="form.type !== 'dockerBuild'" label="编译命令" prop="versionCmd">
-                <el-input v-model="form.versionCmd" placeholder="例如 mvn -v" />
-                <div class="form-tip">shell 类型需要使用绝对路径</div>
-                <div class="form-tip">dockerRun 类型直接使用相应命令即可</div>
-              </el-form-item>
-            </div>
-
-            <el-form-item style="margin-top: 40px">
-              <el-button v-if="active > 0" @click="active--">上一步</el-button>
-              <el-button v-if="active < 1" type="primary" @click="nextStep">下一步</el-button>
-              <el-button v-if="active === 1" type="success" @click="onAddCompiler">提交保存</el-button>
-            </el-form-item>
-          </el-form>
-        </div>
-      </template>
+      <compiler-add-card
+        @close="showAddCompilerDialog = false"
+        @success="handleAddSuccess"
+      />
     </el-dialog>
     <!-- dockerfile 变量 -->
     <el-dialog
@@ -342,17 +288,20 @@
 </template>
 
 <script>
+import CompilerAddCard from '@/components/card/CompilerAddCard.vue'
 import {
-  addCompiler, addContainerBind,
+  addContainerBind,
   deleteCompiler,
   deleteContainerBind, getArgList,
   getCompilerList,
-  getCompilerTypes,
   getContainerBindList, getContainerEnvList
 } from '@/api/devops'
 
 export default {
   name: 'Compiler',
+  components: {
+    CompilerAddCard
+  },
   data() {
     return {
       queryInfo: {
@@ -366,17 +315,7 @@ export default {
       totalSize: 0,
       dataList: [],
       // **********************************************************************
-      active: 0,
-      // **********************************************************************
       showAddDialog: false,
-      form: {
-        type: '',
-        name: '',
-        compileCmd: '',
-        versionCmd: '',
-        compilerImage: ''
-      },
-      compileTypes: [],
       // **********************************************************************
       showArgDialog: false,
       showAddArgDialog: false,
@@ -405,23 +344,6 @@ export default {
       }
     }
   },
-  computed: {
-    dynamicRules() {
-      const baseRules = {
-        type: [{ required: true, message: '请选择类型', trigger: 'blur' }],
-        name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
-      }
-      // 根据选择的类型增加特定规则
-      if (this.form.type === 'dockerRun') {
-        baseRules.compilerImage = [{ required: true, message: 'dockerRun 类型必须指定编译使用的镜像', trigger: 'blur' }]
-      }
-      if (this.form.type === 'shell' || this.form.type === 'dockerRun') {
-        baseRules.compileCmd = [{ required: true, message: 'shell 和 dockerRun 类型必须填写编译命令', trigger: 'blur' }]
-        baseRules.versionCmd = [{ required: true, message: 'shell 和 dockerRun 类型必须填写编译器的版本命令', trigger: 'blur' }]
-      }
-      return baseRules
-    }
-  },
   created() {
     document.title = '编译器列表'
     this.getData()
@@ -447,65 +369,8 @@ export default {
         this.$message.error(error.message)
       })
     },
-    handleShowAdd(index, row) {
-      getCompilerTypes().then(resp => {
-        if (resp.code === 0) {
-          this.showAddDialog = true
-          this.compileTypes = resp.data
-        } else {
-          this.$message.error(resp.msg)
-        }
-      }).catch(error => {
-        this.$message.error(error.message)
-      })
-    },
-    onAddCompiler1() {
-      const formData = new FormData()
-      formData.append('type', this.form.type)
-      formData.append('name', this.form.name)
-      formData.append('compileCmd', this.form.compileCmd)
-      formData.append('compilerImage', this.form.compilerImage)
-      addCompiler(formData).then(resp => {
-        this.$message.info(resp.msg)
-        this.getData()
-      }).catch(error => {
-        this.$message.error(error.message)
-      }).finally(() => {
-        this.showAddDialog = false
-      })
-    },
-    // 异步校验并跳转
-    async nextStep() {
-      const stepFields = ['type', 'name'] // 第一步需要校验的字段
-
-      try {
-        // 关键:将所有字段的校验转为 Promise 数组
-        const checkActions = stepFields.map(field => {
-          return new Promise((resolve, reject) => {
-            this.$refs.form.validateField(field, error => {
-              if (error) reject(error)
-              else resolve()
-            })
-          })
-        })
-
-        await Promise.all(checkActions)
-        this.active++ // 只有全部校验成功才加 1
-      } catch (e) {
-        this.$message.warning('请检查基础配置是否填写完整')
-      }
-    },
-    onAddCompiler() {
-      this.$refs.form.validate((valid) => {
-        if (valid) {
-          // 这里发起 API 请求
-          console.log('Final Data:', this.form)
-          this.$message.success('编译器配置添加成功!')
-          // 逻辑处理,如关闭弹窗或跳转列表
-        } else {
-          return false
-        }
-      })
+    handleShowAdd() {
+      this.showAddDialog = true
     },
     // **********************************************************************
     handleShowArg(row) {
@@ -690,39 +555,13 @@ export default {
     },
     handleUsage(row) {
       this.$message.info('handleUsage')
+    },
+    handleAddSuccess() {
+      this.getData()
     }
   }
 }
 </script>
 
 <style scoped>
-.compiler-config-steps {
-  max-width: 800px;
-  margin: 0 auto;
-  padding: 20px;
-  background: #fff;
-  border-radius: 8px;
-}
-
-.form-tip {
-  font-size: 12px;
-  color: #909399;
-  line-height: 2;
-}
-
-/* 使用 ::v-deep 穿透 scoped 限制
-  这是 Vue 2 中兼容性最好的写法,支持 Less/Sass
-*/
-::v-deep .el-textarea__inner {
-  font-family: 'Courier New', Courier, monospace;
-  background-color: #f8f9fa;
-  color: #2c3e50;
-  border: 1px solid #dcdfe6;
-}
-
-/* 鼠标悬停和聚焦时的效果优化 */
-::v-deep .el-textarea__inner:focus {
-  background-color: #fff;
-  border-color: #409EFF;
-}
 </style>

+ 13 - 267
src/views/devops/build/Packer.vue

@@ -112,133 +112,10 @@
       :visible.sync="showAddDialog"
       center
     >
-      <template>
-        <div class="compiler-config-steps">
-          <el-steps :active="active" finish-status="success" simple style="margin-bottom: 30px">
-            <el-step title="配置基础环境" icon="el-icon-monitor" />
-            <el-step title="配置打包" icon="el-icon-edit" />
-          </el-steps>
-
-          <el-form ref="form" :model="form" :rules="dynamicRules" label-width="130px">
-            <div v-show="active === 0">
-              <el-form-item label="打包类型" prop="type">
-                <el-select v-model="form.type" placeholder="请选择打包类型" style="width: 100%">
-                  <el-option
-                      v-for="(item, index) in packTypes"
-                      :key="index"
-                      :label="item.label"
-                      :value="item.value"
-                  />
-                </el-select>
-              </el-form-item>
-              <el-form-item label="配置名称" prop="name">
-                <el-input v-model="form.name" placeholder="例如 docker-pack" />
-              </el-form-item>
-            </div>
-
-            <div v-show="active === 1">
-              <div v-if="form.type === 'docker'">
-                <el-form-item label="docker 仓库" prop="dockerRegistry">
-                  <div style="display: flex; align-items: center;">
-                    <el-select v-model="form.dockerRegistry" placeholder="请选择 docker 仓库" style="width: 100%">
-                      <el-option
-                          v-for="(item, index) in registryList"
-                          :key="index"
-                          :label="item.label"
-                          :value="item.value"
-                      />
-                    </el-select>
-                    <el-tooltip content="添加 docker 仓库" placement="top">
-                      <el-button
-                          type="primary"
-                          icon="el-icon-plus"
-                          circle
-                          size="mini"
-                          style="margin-left: 10px"
-                          @click="goToAddRegistry"
-                      />
-                    </el-tooltip>
-                  </div>
-                  <div class="form-tip">编译构建生成的镜像会推送到该仓库</div>
-                </el-form-item>
-              </div>
-              <div v-else-if="form.type === 'ossStatic'" class="oss-config-group">
-                <el-form-item prop="artifactPath">
-                  <span slot="label">
-                    编译产物路径
-                    <el-tooltip content="编译完成后产物所在的相对路径,例如:dist/ 或 target/app.jar" placement="top">
-                      <i class="el-icon-question" style="cursor: help; color: #909399;"></i>
-                    </el-tooltip>
-                  </span>
-                  <el-input v-model="form.artifactPath" placeholder="例如 dist/" clearable />
-                  <div class="form-tip">源码根目录下的相对路径</div>
-                </el-form-item>
-                <el-form-item label="阿里云 OSS" prop="ossEndpoint">
-                  <div class="oss-select-wrapper">
-                    <el-select
-                        v-model="form.ossEndpoint"
-                        placeholder="请选择阿里云 OSS"
-                        style="flex: 1"
-                        filterable
-                    >
-                      <el-option
-                          v-for="(item, index) in ossList"
-                          :key="index"
-                          :label="item.label"
-                          :value="item.value"
-                      />
-                    </el-select>
-
-                    <el-tooltip content="添加新的 OSS" placement="top">
-                      <el-button
-                          type="primary"
-                          icon="el-icon-plus"
-                          circle
-                          size="small"
-                          class="add-bucket-btn"
-                          @click="goToAddOss"
-                      />
-                    </el-tooltip>
-                  </div>
-                  <div class="form-tip">编译生成的静态产物将自动上传并同步至该 OSS, 应用部署配置中会指定使用的 bucket</div>
-                </el-form-item>
-              </div>
-              <div v-else>
-                <el-form-item prop="artifactPath">
-                  <span slot="label">
-                    编译产物路径
-                    <el-tooltip content="编译完成后产物所在的相对路径,例如:dist/ 或 target/app.jar" placement="top">
-                      <i class="el-icon-question" style="cursor: help; color: #909399;"></i>
-                    </el-tooltip>
-                  </span>
-                  <el-input v-model="form.artifactPath" placeholder="例如 dist/" clearable />
-                  <div class="form-tip">源码根目录下的相对路径</div>
-                </el-form-item>
-              </div>
-            </div>
-
-            <el-form-item style="margin-top: 40px">
-              <el-button v-if="active > 0" @click="active--">上一步</el-button>
-              <el-button v-if="active < 1" type="primary" @click="nextStep">下一步</el-button>
-              <el-button v-if="active === 1" type="success" @click="onAddPacker">提交保存</el-button>
-            </el-form-item>
-          </el-form>
-        </div>
-      </template>
-    </el-dialog>
-    <el-dialog
-        title="添加 docker 仓库"
-        append-to-body
-        :visible.sync="showAddRegistryDialog"
-        center
-    >
-    </el-dialog>
-    <el-dialog
-      title="添加阿里云 OSS"
-      append-to-body
-      :visible.sync="showAddOssDialog"
-      center
-    >
+      <packer-add-card
+        @close="showAddDialog = false"
+        @success="handleAddSuccess"
+      />
     </el-dialog>
     <el-dialog
       title="存储位置"
@@ -251,10 +128,14 @@
 </template>
 
 <script>
-import { addPacker, deletePacker, getPackerList, getPackTypes } from '@/api/devops'
+import PackerAddCard from '@/components/card/PackerAddCard.vue'
+import { deletePacker, getPackerList } from '@/api/devops'
 
 export default {
   name: 'Packer',
+  components: {
+    PackerAddCard
+  },
   data() {
     return {
       queryInfo: {
@@ -268,29 +149,7 @@ export default {
       totalSize: 0,
       dataList: [],
       // **********************************************************************
-      active: 0,
       showAddDialog: false,
-      form: {
-        type: '',
-        name: '',
-        artifactPath: '',
-        dockerRegistry: '',
-        ossEndpoint: '',
-        targetPath: ''
-      },
-      packTypes: [],
-      registryList: [],
-      ossList: [],
-      // **********************************************************************
-      showAddRegistryDialog: false,
-      registryForm: {
-        registryUrl: ''
-      },
-      // **********************************************************************
-      showAddOssDialog: false,
-      ossForm: {
-        ossEndpoint: ''
-      },
       // **********************************************************************
       showTargetDialog: false,
       ossForm1: {
@@ -298,19 +157,6 @@ export default {
       }
     }
   },
-  computed: {
-    dynamicRules() {
-      const baseRules = {
-        type: [{ required: true, message: '请选择类型', trigger: 'blur' }],
-        name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
-      }
-      // 根据选择的类型增加特定规则
-      if (this.form.type === 'docker') {
-        baseRules.dockerRegistry = [{ required: true, message: 'docker 类型必须指定使用的仓库', trigger: 'blur' }]
-      }
-      return baseRules
-    }
-  },
   created() {
     document.title = '打包配置列表'
     this.getData()
@@ -336,68 +182,11 @@ export default {
         this.$message.error(error.message)
       })
     },
-    handleShowAdd(index, row) {
-      getPackTypes().then(resp => {
-        if (resp.code === 0) {
-          this.showAddDialog = true
-          this.packTypes = resp.data.packTypes
-          this.registryList = resp.data.registryList
-          this.ossList = resp.data.ossList
-        } else {
-          this.$message.error(resp.msg)
-        }
-      }).catch(error => {
-        this.$message.error(error.message)
-      })
-    },
-    onAddPacker1() {
-      const formData = new FormData()
-      formData.append('type', this.form.type)
-      formData.append('name', this.form.name)
-      formData.append('artifactPath', this.form.artifactPath)
-      formData.append('dockerRegistry', this.form.dockerRegistry)
-      formData.append('targetPath', this.form.targetPath)
-      addPacker(formData).then(resp => {
-        this.$message.info(resp.msg)
-        this.getData()
-      }).catch(error => {
-        this.$message.error(error.message)
-      }).finally(() => {
-        this.showAddDialog = false
-      })
+    handleShowAdd() {
+      this.showAddDialog = true
     },
-    // 异步校验并跳转
-    async nextStep() {
-      const stepFields = ['type', 'name'] // 第一步需要校验的字段
-
-      try {
-        // 关键:将所有字段的校验转为 Promise 数组
-        const checkActions = stepFields.map(field => {
-          return new Promise((resolve, reject) => {
-            this.$refs.form.validateField(field, error => {
-              if (error) reject(error)
-              else resolve()
-            })
-          })
-        })
-
-        await Promise.all(checkActions)
-        this.active++ // 只有全部校验成功才加 1
-      } catch (e) {
-        this.$message.warning('请检查基础配置是否填写完整')
-      }
-    },
-    onAddPacker() {
-      this.$refs.form.validate((valid) => {
-        if (valid) {
-          // 这里发起 API 请求
-          console.log('Final Data:', this.form)
-          this.$message.success('打包配置添加成功!')
-          // 逻辑处理,如关闭弹窗或跳转列表
-        } else {
-          return false
-        }
-      })
+    handleAddSuccess() {
+      this.$message.success('打包配置添加成功!')
     },
     handleEdit(index, row) {
       this.$confirm('确定要删除 ' + row.name + '?', '提示', {
@@ -420,12 +209,6 @@ export default {
         })
       })
     },
-    goToAddRegistry() {
-      this.showAddRegistryDialog = true
-    },
-    goToAddOss() {
-      this.showAddOssDialog = true
-    },
     handleUsage(row) {
       this.$message.info('handleUsage')
     },
@@ -437,41 +220,4 @@ export default {
 </script>
 
 <style>
-/* OSS 配置组容器,可以加一个淡色背景或边框区分 */
-.oss-config-group {
-  padding: 15px;
-  background-color: #fcfcfc;
-  border-radius: 4px;
-  border: 1px dashed #e4e7ed;
-  margin-bottom: 20px;
-}
-
-/* Flex 布局让选择框和按钮完美对齐 */
-.oss-select-wrapper {
-  display: flex;
-  align-items: center;
-  gap: 12px; /* 间距更加现代化 */
-}
-
-/* 按钮悬停动画 */
-.add-bucket-btn {
-  transition: transform 0.2s;
-}
-.add-bucket-btn:hover {
-  transform: scale(1.1);
-}
-
-/* 提示文字样式优化 */
-.form-tip {
-  font-size: 12px;
-  color: #909399;
-  line-height: 1.5;
-  margin-top: 4px;
-}
-
-/* 统一图标颜色 */
-.el-icon-question {
-  font-size: 14px;
-  vertical-align: middle;
-}
 </style>

+ 15 - 199
src/views/devops/build/RepoAuth.vue

@@ -83,87 +83,23 @@
       :visible.sync="showAddDialog"
       center
     >
-      <div class="compiler-config-steps">
-        <el-steps :active="active" finish-status="success" simple style="margin-bottom: 30px">
-          <el-step title="基础配置" icon="el-icon-monitor" />
-          <el-step title="认证配置" icon="el-icon-edit" />
-        </el-steps>
-
-        <el-form ref="form" :model="form" :rules="dynamicRules" label-width="130px">
-          <div v-show="active === 0">
-            <el-form-item label="认证类型" prop="authType">
-              <el-select v-model="form.authType" placeholder="请选择认证类型" style="width: 100%">
-                <el-option
-                    v-for="(item, index) in authTypes"
-                    :key="index"
-                    :label="item.label"
-                    :value="item.value"
-                />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="认证名称" prop="name">
-              <el-input v-model="form.name" placeholder="例如 git-auth" />
-            </el-form-item>
-            <el-form-item label="仓库类型" prop="type">
-              <el-select v-model="form.type" placeholder="请选择仓库类型" style="width: 100%">
-                <el-option
-                    v-for="(item, index) in repoTypes"
-                    :key="index"
-                    :label="item.label"
-                    :value="item.value"
-                />
-              </el-select>
-            </el-form-item>
-          </div>
-
-          <div v-show="active === 1">
-            <div v-if="form.authType === 'ssh'">
-              <el-form-item label="RSA 私钥" prop="rsaPrikey">
-                <el-input
-                    v-model="form.rsaPrikey"
-                    type="textarea"
-                    :autosize="{ minRows: 4 }"
-                    placeholder="请填写 RSA 私钥"
-                />
-              </el-form-item>
-              <el-form-item label="RSA 公钥" prop="rsaPubkey">
-                <el-input
-                    v-model="form.rsaPubkey"
-                    type="textarea"
-                    :autosize="{ minRows: 4 }"
-                    placeholder="请填写 RSA 公钥"
-                />
-              </el-form-item>
-            </div>
-            <div v-else-if="form.authType === 'http'">
-              <el-form-item label="用户名" prop="username">
-                <el-input v-model="form.username" placeholder="请填写用户名" />
-              </el-form-item>
-              <el-form-item label="密码" prop="password">
-                <el-input v-model="form.password" placeholder="请填写密码" />
-              </el-form-item>
-            </div>
-            <div v-else>
-              <el-input value="none 认证类型不需要填写任何认证资料" readonly />
-            </div>
-          </div>
-
-          <el-form-item style="margin-top: 40px">
-            <el-button v-if="active > 0" @click="active--">上一步</el-button>
-            <el-button v-if="active < 1" type="primary" @click="nextStep">下一步</el-button>
-            <el-button v-if="active === 1" type="success" @click="onAddRepoAuth">提交保存</el-button>
-          </el-form-item>
-        </el-form>
-      </div>
+      <repo-auth-add-card
+        @close="showAddDialog = false"
+        @success="handleAddSuccess"
+      />
     </el-dialog>
   </el-container>
 </template>
 
 <script>
-import { addRepoAuth, deleteRepoAuth, getRepoAuthList, getRepoTypes } from '@/api/devops'
+import RepoAuthAddCard from '@/components/card/RepoAuthAddCard.vue'
+import { deleteRepoAuth, getRepoAuthList } from '@/api/devops'
 
 export default {
   name: 'RepoAuth',
+  components: {
+    RepoAuthAddCard
+  },
   data() {
     return {
       queryInfo: {
@@ -177,37 +113,7 @@ export default {
       totalSize: 0,
       dataList: [],
       // **********************************************************************
-      showAddDialog: false,
-      active: 0,
-      form: {
-        authType: 'http',
-        name: '',
-        type: 'git',
-        username: '',
-        password: '',
-        rsaPrikey: '',
-        rsaPubkey: '',
-      },
-      repoTypes: [],
-      authTypes: []
-    }
-  },
-  computed: {
-    dynamicRules() {
-      const baseRules = {
-        type: [{ required: true, message: '请选择仓库类型', trigger: 'blur' }],
-        name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-        authType: [{ required: true, message: '请选择认证类型', trigger: 'blur' }]
-      }
-      // 根据选择的类型增加特定规则
-      if (this.form.authType === 'ssh') {
-        baseRules.rsaPrikey = [{ required: true, message: 'ssh 认证类型必须填写 RSA 私钥', trigger: 'blur' }]
-        baseRules.rsaPubkey = [{ required: true, message: 'ssh 认证类型必须填写 RSA 公钥', trigger: 'blur' }]
-      } else if (this.form.authType === 'http') {
-        baseRules.username = [{ required: true, message: 'http 认证类型必须填写 username', trigger: 'blur' }]
-        baseRules.password = [{ required: true, message: 'http 认证类型必须填写 password', trigger: 'blur' }]
-      }
-      return baseRules
+      showAddDialog: false
     }
   },
   created() {
@@ -235,72 +141,8 @@ export default {
         this.$message.error(error.message)
       })
     },
-    handleShowAdd(index, row) {
-      getRepoTypes().then(resp => {
-        if (resp.code === 0) {
-          this.repoTypes = resp.data.repoTypes
-          this.authTypes = resp.data.authTypes
-          this.showAddDialog = true
-        } else {
-          this.$message.error(resp.msg)
-        }
-      }).catch(error => {
-        this.$message.error(error.message)
-      })
-    },
-    onAddRepoAuth1() {
-      const formData = new FormData()
-      formData.append('type', this.form.type)
-      formData.append('name', this.form.name)
-      formData.append('authType', this.form.authType)
-      formData.append('username', this.form.username)
-      formData.append('password', this.form.password)
-      addRepoAuth(formData).then(resp => {
-        this.$message.info(resp.msg)
-        this.getData()
-      }).catch(error => {
-        this.$message.error(error.message)
-      }).finally(() => {
-        this.showAddDialog = false
-      })
-    },
-    // 异步校验并跳转
-    async nextStep() {
-      const stepFields = ['type', 'authType', 'name'] // 第一步需要校验的字段
-
-      try {
-        // 关键:将所有字段的校验转为 Promise 数组
-        const checkActions = stepFields.map(field => {
-          return new Promise((resolve, reject) => {
-            this.$refs.form.validateField(field, error => {
-              if (error) reject(error)
-              else resolve()
-            })
-          })
-        })
-
-        await Promise.all(checkActions)
-        this.active++ // 只有全部校验成功才加 1
-      } catch (e) {
-        this.$message.warning('请检查基础配置是否填写完整')
-      }
-    },
-    onAddRepoAuth() {
-      this.$refs.form.validate((valid) => {
-        if (valid) {
-          addRepoAuth(this.form).then(resp => {
-            this.$message.info(resp.msg)
-            this.$refs.form.resetFields();
-            this.getData()
-          }).catch(error => {
-            this.$message.error(error.message)
-          }).finally(() => {
-            this.showAddDialog = false
-          })
-        } else {
-          return false
-        }
-      })
+    handleShowAdd() {
+      this.showAddDialog = true
     },
     handleEdit(index, row) {
       this.$confirm('确定要删除 ' + row.name + '?', '提示', {
@@ -325,39 +167,13 @@ export default {
     },
     handleUsage(row) {
       this.$message.info('handleUsage')
+    },
+    handleAddSuccess() {
+      this.getData()
     }
   }
 }
 </script>
 
 <style scoped>
-.compiler-config-steps {
-  max-width: 800px;
-  margin: 0 auto;
-  padding: 20px;
-  background: #fff;
-  border-radius: 8px;
-}
-
-.form-tip {
-  font-size: 12px;
-  color: #909399;
-  line-height: 2;
-}
-
-/* 使用 ::v-deep 穿透 scoped 限制
-  这是 Vue 2 中兼容性最好的写法,支持 Less/Sass
-*/
-::v-deep .el-textarea__inner {
-  font-family: 'Courier New', Courier, monospace;
-  background-color: #f8f9fa;
-  color: #2c3e50;
-  border: 1px solid #dcdfe6;
-}
-
-/* 鼠标悬停和聚焦时的效果优化 */
-::v-deep .el-textarea__inner:focus {
-  background-color: #fff;
-  border-color: #409EFF;
-}
 </style>

+ 29 - 3
src/views/devops/machine/AliyunKey.vue

@@ -1,7 +1,8 @@
 <template>
-  <el-container>
+  <div>
     <el-header>
       <h3>阿里云帐号列表</h3>
+      <el-button type="text" style="margin-left: 5px" @click="handleAdd">添加</el-button>
     </el-header>
     <el-main>
       <el-table
@@ -57,14 +58,32 @@
         </el-table-column>
       </el-table>
     </el-main>
-  </el-container>
+
+    <el-dialog
+      title="添加阿里云 OSS"
+      :visible.sync="showAddOssDialog"
+      width="650px"
+      :close-on-click-modal="false"
+      append-to-body
+      destroy-on-close
+    >
+      <aliyun-add-card
+        @close="showAddOssDialog = false"
+        @success="onAddSuccess"
+      />
+    </el-dialog>
+  </div>
 </template>
 
 <script>
+import AliyunAddCard from '@/components/card/AliyunAddCard.vue'
 import { getAliyunKeyList } from '@/api/devops'
 
 export default {
   name: 'AliyunKey',
+  components: {
+    AliyunAddCard
+  },
   data() {
     return {
       // 屏幕宽度, 为了控制分页条的大小
@@ -72,7 +91,8 @@ export default {
       currentPage: 1,
       pageSize: 10,
       totalSize: 0,
-      dataList: []
+      dataList: [],
+      showAddOssDialog: false
     }
   },
   created() {
@@ -97,6 +117,12 @@ export default {
     },
     handleUsage(row) {
       this.$message.info('handleUsage')
+    },
+    handleAdd() {
+      this.showAddOssDialog = true
+    },
+    onAddSuccess() {
+      this.getData()
     }
   }
 }