3 İşlemeler d760ad0444 ... 5e708af0e3

Yazar SHA1 Mesaj Tarih
  reghao 5e708af0e3 调整构建配置相关页面 23 saat önce
  reghao 1f4e59c94e RepoAuth.vue 页面使用分步表单添加仓库认证配置 1 gün önce
  reghao 6431292c25 Compiler.vue 页面使用分步表单添加编译器配置 1 gün önce

+ 16 - 8
src/api/devops.js

@@ -13,7 +13,7 @@ const devopsApi = {
   getMachineTaskList: '/api/devops/machine/task',
   getMachineNginx: '/api/search1/nginx',
   getMachineNginx1: '/api/devops/srv/nginx',
-  getDockerList: '/api/devops/docker',
+  getDockerList: '/api/devops/build/docker',
   getCompilerList: '/api/devops/build/compiler',
   getRepoAuthList: '/api/devops/build/repoauth',
   getPackerList: '/api/devops/build/packer',
@@ -177,6 +177,10 @@ export function eraseBuildDir() {
   return post(devopsApi.eraseBuildDir)
 }
 
+export function getRepoAuthNames() {
+  return get(devopsApi.getRepoAuthList + "/kv")
+}
+
 export function getRepoAuthList(pn) {
   return get(devopsApi.getRepoAuthList + '?pn=' + pn)
 }
@@ -201,22 +205,26 @@ export function getCompilerTypes() {
   return get(devopsApi.getCompilerList + '/types')
 }
 
-export function getCompilerVersion(id) {
-  return get(devopsApi.getCompilerList + '/version?id=' + id)
-}
-
-export function addImageBind(form) {
+export function addContainerBind(form) {
   return post(devopsApi.getCompilerList + '/bind/add', form)
 }
 
-export function deleteImageBind(form) {
+export function deleteContainerBind(form) {
   return post(devopsApi.getCompilerList + '/bind/delete', form)
 }
 
-export function getImageBindList(queryInfo) {
+export function getContainerBindList(queryInfo) {
   return get(devopsApi.getCompilerList + '/bind/list', queryInfo)
 }
 
+export function getContainerEnvList(queryInfo) {
+  return get(devopsApi.getCompilerList + '/env/list', queryInfo)
+}
+
+export function getArgList(queryInfo) {
+  return get(devopsApi.getCompilerList + '/arg/list', queryInfo)
+}
+
 export function addCompiler(formData) {
   return postForm(devopsApi.getCompilerList, formData)
 }

+ 9 - 9
src/router/background_devops.js

@@ -20,10 +20,10 @@ const NginxLog = () => import('views/devops/srv/NginxLog')
 // build
 const BuildDir = () => import('views/devops/build/BuildDir')
 const RepoAuth = () => import('views/devops/build/RepoAuth')
+const DockerRegistry = () => import('views/devops/docker/DockerRegistry')
 const Compiler = () => import('views/devops/build/Compiler')
 const Packer = () => import('views/devops/build/Packer')
 // docker
-const DockerRegistry = () => import('views/devops/docker/DockerRegistry')
 const DockerImage = () => import('views/devops/docker/DockerImage')
 const Docker = () => import('views/devops/docker/Docker')
 // app
@@ -109,14 +109,6 @@ export default {
       component: { render: (e) => e('router-view') },
       meta: { needAuth: true, roles: ['devops_admin'] },
       children: [
-        {
-          path: '/bg/docker/registry',
-          name: 'DockerRegistry',
-          title: '镜像仓库',
-          icon: 'el-icon-user-solid',
-          component: DockerRegistry,
-          meta: { needAuth: true, roles: ['devops_admin'] }
-        },
         {
           path: '/bg/docker/image',
           name: 'DockerImage',
@@ -151,6 +143,14 @@ export default {
           component: RepoAuth,
           meta: { needAuth: true, roles: ['devops_admin'] }
         },
+        {
+          path: '/bg/build/docker_registry',
+          name: 'DockerRegistry',
+          title: 'docker 仓库',
+          icon: 'el-icon-user-solid',
+          component: DockerRegistry,
+          meta: { needAuth: true, roles: ['devops_admin'] }
+        },
         {
           path: '/bg/build/compiler',
           name: 'Compiler',

+ 281 - 79
src/views/devops/build/Compiler.vue

@@ -30,7 +30,7 @@
               size="mini"
               type="success"
               icon="el-icon-edit"
-              @click="handleShowImage(scope.$index, scope.row)"
+              @click="handleShowArg(scope.row)"
             >编辑</el-button>
           </template>
         </el-table-column>
@@ -43,7 +43,7 @@
               size="mini"
               type="success"
               icon="el-icon-edit"
-              @click="handleShowImage(scope.$index, scope.row)"
+              @click="handleShowBind(scope.row)"
             >编辑</el-button>
           </template>
         </el-table-column>
@@ -56,7 +56,7 @@
               size="mini"
               type="success"
               icon="el-icon-edit"
-              @click="handleShowImage(scope.$index, scope.row)"
+              @click="handleShowEnv(scope.row)"
             >编辑</el-button>
           </template>
         </el-table-column>
@@ -74,7 +74,7 @@
             <el-button
               size="mini"
               type="danger"
-              @click="handleEdit(scope.$index, scope.row)"
+              @click="handleEdit(scope.row)"
             >删除</el-button>
           </template>
         </el-table-column>
@@ -106,7 +106,7 @@
             <el-step title="编写编译脚本" icon="el-icon-edit" />
           </el-steps>
 
-          <el-form ref="form" :model="form" :rules="rules" label-width="130px">
+          <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%">
@@ -119,33 +119,28 @@
                 </el-select>
               </el-form-item>
               <el-form-item label="配置名称" prop="name">
-                <el-input v-model="form.name" placeholder="例如:Dotnet8-Standard-Build" />
+                <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="e.g. mcr.microsoft.com/dotnet/sdk:8.0" />
+                <el-input v-model="form.compilerImage" placeholder="例如 amazoncorretto:17.0.16-al2-native-jdk" />
                 <div class="form-tip">该镜像将作为构建时的 Runtime 容器</div>
-              </el-form-item>
-              <el-form-item v-if="form.type === 'shell'" label="编译主目录" prop="homePath">
-                <el-input v-model="form.homePath" placeholder="例如:/src 或 /app" />
-              </el-form-item>
-              <el-form-item v-if="form.type !== 'dockerBuild'" label="编译器版本命令" prop="versionCmd">
-                <el-input
-                  v-model="form.versionCmd"
-                  type="textarea"
-                  :autosize="{ minRows: 2 }"
-                  placeholder="例如:dotnet --version"
-                />
+                <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="例如:dotnet publish -c Release -o ./publish"
+                  placeholder="例如 mvn clean package -Dmaven.test.skip"
                 />
+                <div class="form-tip">shell 类型需要使用绝对路径</div>
+                <div class="form-tip">dockerRun 类型直接使用相应命令即可</div>
               </el-form-item>
             </div>
 
@@ -158,17 +153,124 @@
         </div>
       </template>
     </el-dialog>
-    <!-- 查看 docker 目录映射对话框 -->
+    <!-- dockerfile 变量 -->
+    <el-dialog
+        title="dockerfile 变量"
+        append-to-body
+        :visible.sync="showArgDialog"
+        center
+    >
+      <template>
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="handleAddArg">添加</el-button>
+        <el-table
+            :data="dockerfileArgs"
+            style="width: 100%"
+        >
+          <el-table-column
+              prop="argName"
+              label="变量名"
+          />
+          <el-table-column
+              prop="argValue"
+              label="变量值"
+          />
+          <el-table-column
+              fixed="right"
+              label="操作"
+              width="120"
+          >
+            <template slot-scope="scope">
+              <el-button
+                  size="mini"
+                  type="danger"
+                  @click="onDeleteArg(scope.row)"
+              >删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+    </el-dialog>
     <el-dialog
-      title="docker 目录映射"
+        title="添加 dockerfile 变量"
+        append-to-body
+        :visible.sync="showAddArgDialog"
+        center
+    >
+      <template>
+        <el-form ref="form" :model="dockerfileArgForm" label-width="80px">
+          <el-form-item label="变量名" style="width: 70%; padding-right: 2px">
+            <el-input v-model="dockerfileArgForm.argName" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item label="变量值" style="width: 70%; padding-right: 2px">
+            <el-input v-model="dockerfileArgForm.argValue" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onAddArg">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+    <!-- 容器环境变量 -->
+    <el-dialog
+        title="容器环境变量"
+        append-to-body
+        :visible.sync="showEnvDialog"
+        center
+    >
+      <template>
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="handleAddEnv">添加</el-button>
+        <el-table
+            :data="containerEnvs"
+            style="width: 100%"
+        >
+          <el-table-column
+              prop="env"
+              label="环境变量"
+          />
+          <el-table-column
+              fixed="right"
+              label="操作"
+              width="120"
+          >
+            <template slot-scope="scope">
+              <el-button
+                  size="mini"
+                  type="danger"
+                  @click="onDeleteEnv(scope.row)"
+              >删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+    </el-dialog>
+    <el-dialog
+        title="添加容器环境变量"
+        append-to-body
+        :visible.sync="showAddEnvDialog"
+        center
+    >
+      <template>
+        <el-form ref="form" :model="containerEnvForm" label-width="80px">
+          <el-form-item label="容器环境变量" style="width: 70%; padding-right: 2px">
+            <el-input v-model="containerEnvForm.env" style="width: 70%; padding-right: 2px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onAddEnv">确定</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </el-dialog>
+    <!-- 容器目录映射 -->
+    <el-dialog
+      title="容器目录映射"
       append-to-body
-      :visible.sync="showImageDialog"
+      :visible.sync="showBindDialog"
       center
     >
       <template>
         <el-button type="success" size="mini" icon="el-icon-plus" @click="handleAddBind">添加</el-button>
         <el-table
-          :data="dockerBinds"
+          :data="containerBinds"
           style="width: 100%"
         >
           <el-table-column
@@ -188,7 +290,7 @@
               <el-button
                 size="mini"
                 type="danger"
-                @click="onDeleteBind(scope.$index, scope.row)"
+                @click="onDeleteBind(scope.row)"
               >删除</el-button>
             </template>
           </el-table-column>
@@ -196,18 +298,18 @@
       </template>
     </el-dialog>
     <el-dialog
-      title="添加 docker 目录映射"
+      title="添加容器目录映射"
       append-to-body
       :visible.sync="showAddBindDialog"
       center
     >
       <template>
-        <el-form ref="form" :model="dockerBindForm" label-width="80px">
+        <el-form ref="form" :model="containerBindForm" label-width="80px">
           <el-form-item label="host 目录" style="width: 70%; padding-right: 2px">
-            <el-input v-model="dockerBindForm.hostPath" style="width: 70%; padding-right: 2px" />
+            <el-input v-model="containerBindForm.hostPath" style="width: 70%; padding-right: 2px" />
           </el-form-item>
           <el-form-item label="container 目录" style="width: 70%; padding-right: 2px">
-            <el-input v-model="dockerBindForm.containerPath" style="width: 70%; padding-right: 2px" />
+            <el-input v-model="containerBindForm.containerPath" style="width: 70%; padding-right: 2px" />
           </el-form-item>
           <el-form-item>
             <el-button type="primary" @click="onAddBind">确定</el-button>
@@ -215,27 +317,17 @@
         </el-form>
       </template>
     </el-dialog>
-    <el-dialog
-      :title="title"
-      append-to-body
-      :visible.sync="showVersionDialog"
-      center
-    >
-      <template>
-        <span v-html="versionResult" />
-      </template>
-    </el-dialog>
   </el-container>
 </template>
 
 <script>
 import {
-  addCompiler, addImageBind,
+  addCompiler, addContainerBind,
   deleteCompiler,
-  deleteImageBind,
+  deleteContainerBind, getArgList,
   getCompilerList,
-  getCompilerTypes, getCompilerVersion,
-  getImageBindList
+  getCompilerTypes,
+  getContainerBindList, getContainerEnvList
 } from '@/api/devops'
 
 export default {
@@ -254,37 +346,57 @@ export default {
       dataList: [],
       // **********************************************************************
       active: 0,
-      rules: {
-        type: [{ required: true, message: '请选择类型', trigger: 'change' }],
-        name: [{ required: true, message: '请输入配置名字', trigger: 'blur' }],
-        compilerImage: [{ required: true, message: '必须指定编译镜像', trigger: 'blur' }],
-        homePath: [{ required: true, message: '主目录不能为空', trigger: 'blur' }],
-        compileCmd: [{ required: true, message: '编译命令是核心,必须填写', trigger: 'blur' }]
-      },
       // **********************************************************************
       showAddDialog: false,
       form: {
         type: '',
         name: '',
-        homePath: '',
         compileCmd: '',
-        versionCmd: '',
         compilerImage: ''
       },
       compileTypes: [],
       // **********************************************************************
-      showImageDialog: false,
+      showArgDialog: false,
+      showAddArgDialog: false,
+      dockerfileArgs: [],
+      dockerfileArgForm: {
+        id: 0,
+        argName: '',
+        argValue: ''
+      },
+      // **********************************************************************
+      showEnvDialog: false,
+      showAddEnvDialog: false,
+      containerEnvs: [],
+      containerEnvForm: {
+        id: 0,
+        env: ''
+      },
+      // **********************************************************************
+      showBindDialog: false,
       showAddBindDialog: false,
-      dockerBinds: [],
-      dockerBindForm: {
+      containerBinds: [],
+      containerBindForm: {
         id: 0,
         hostPath: '',
         containerPath: ''
-      },
-      // **********************************************************************
-      showVersionDialog: false,
-      versionResult: '',
-      title: '编译器版本信息'
+      }
+    }
+  },
+  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' }]
+      }
+      return baseRules
     }
   },
   created() {
@@ -328,9 +440,7 @@ export default {
       const formData = new FormData()
       formData.append('type', this.form.type)
       formData.append('name', this.form.name)
-      formData.append('homePath', this.form.homePath)
       formData.append('compileCmd', this.form.compileCmd)
-      formData.append('versionCmd', this.form.versionCmd)
       formData.append('compilerImage', this.form.compilerImage)
       addCompiler(formData).then(resp => {
         this.$message.info(resp.msg)
@@ -374,30 +484,121 @@ export default {
         }
       })
     },
-    handleGetVersion(index, row) {
-      getCompilerVersion(row.id).then(resp => {
+    // **********************************************************************
+    handleShowArg(row) {
+      this.getArgListWrapper(row.id)
+    },
+    getArgListWrapper(id) {
+      const queryInfo = {}
+      queryInfo.id = id
+      getArgList(queryInfo).then(resp => {
+        if (resp.code === 0) {
+          this.dockerfileArgs = resp.data
+          this.showArgDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleAddArg() {
+      this.showAddArgDialog = true
+    },
+    onAddArg() {
+      addContainerBind(this.dockerBindForm).then(resp => {
+        if (resp.code === 0) {
+          this.getArgListWrapper(this.dockerBindForm.id)
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showAddBindDialog = false
+        this.dockerBindForm.hostPath = ''
+        this.dockerBindForm.containerPath = ''
+      })
+    },
+    onDeleteArg(row) {
+      this.dockerBindForm.hostPath = row.hostPath
+      this.dockerBindForm.containerPath = row.containerPath
+      deleteContainerBind(this.dockerBindForm).then(resp => {
+        if (resp.code === 0) {
+          this.getArgListWrapper(this.dockerBindForm.id)
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.dockerBindForm.hostPath = ''
+        this.dockerBindForm.containerPath = ''
+      })
+    },
+    // **********************************************************************
+    handleShowEnv(row) {
+      this.getEnvListWrapper(row.id)
+    },
+    getEnvListWrapper(id) {
+      const queryInfo = {}
+      queryInfo.id = id
+      getContainerEnvList(queryInfo).then(resp => {
         if (resp.code === 0) {
-          this.versionResult = resp.data
-          this.title = row.name + ' 版本信息'
-          this.showVersionDialog = true
+          this.containerEnvs = resp.data
+          this.showEnvDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    handleAddEnv() {
+      this.showAddEnvDialog = true
+    },
+    onAddEnv() {
+      addContainerBind(this.dockerBindForm).then(resp => {
+        if (resp.code === 0) {
+          this.getEnvListWrapper(this.dockerBindForm.id)
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      }).finally(() => {
+        this.showAddBindDialog = false
+        this.dockerBindForm.hostPath = ''
+        this.dockerBindForm.containerPath = ''
+      })
+    },
+    onDeleteEnv(row) {
+      this.dockerBindForm.hostPath = row.hostPath
+      this.dockerBindForm.containerPath = row.containerPath
+      deleteContainerBind(this.dockerBindForm).then(resp => {
+        if (resp.code === 0) {
+          this.getEnvListWrapper(this.dockerBindForm.id)
         } else {
           this.$message.warning(resp.msg)
         }
       }).catch(error => {
         this.$message.error(error.message)
+      }).finally(() => {
+        this.dockerBindForm.hostPath = ''
+        this.dockerBindForm.containerPath = ''
       })
     },
-    handleShowImage(index, row) {
-      this.dockerBindForm.id = row.id
-      this.getImageBindListWrapper(row.id)
+    // **********************************************************************
+    handleShowBind(row) {
+      this.getBindListWrapper(row.id)
     },
-    getImageBindListWrapper(id) {
+    getBindListWrapper(id) {
       const queryInfo = {}
       queryInfo.id = id
-      getImageBindList(queryInfo).then(resp => {
+      getContainerBindList(queryInfo).then(resp => {
         if (resp.code === 0) {
-          this.dockerBinds = resp.data
-          this.showImageDialog = true
+          this.containerBinds = resp.data
+          this.showBindDialog = true
         } else {
           this.$message.error(resp.msg)
         }
@@ -409,9 +610,9 @@ export default {
       this.showAddBindDialog = true
     },
     onAddBind() {
-      addImageBind(this.dockerBindForm).then(resp => {
+      addContainerBind(this.dockerBindForm).then(resp => {
         if (resp.code === 0) {
-          this.getImageBindListWrapper(this.dockerBindForm.id)
+          this.getBindListWrapper(this.dockerBindForm.id)
         } else {
           this.$message.warning(resp.msg)
         }
@@ -423,12 +624,12 @@ export default {
         this.dockerBindForm.containerPath = ''
       })
     },
-    onDeleteBind(index, row) {
+    onDeleteBind(row) {
       this.dockerBindForm.hostPath = row.hostPath
       this.dockerBindForm.containerPath = row.containerPath
-      deleteImageBind(this.dockerBindForm).then(resp => {
+      deleteContainerBind(this.dockerBindForm).then(resp => {
         if (resp.code === 0) {
-          this.getImageBindListWrapper(this.dockerBindForm.id)
+          this.getBindListWrapper(this.dockerBindForm.id)
         } else {
           this.$message.warning(resp.msg)
         }
@@ -439,10 +640,11 @@ export default {
         this.dockerBindForm.containerPath = ''
       })
     },
+    // **********************************************************************
     handleDetail(row) {
       this.$message.info('get ' + row.name + ' detail')
     },
-    handleEdit(index, row) {
+    handleEdit(row) {
       this.$confirm('确定要删除 ' + row.name + '?', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',

+ 137 - 40
src/views/devops/build/Packer.vue

@@ -63,46 +63,88 @@
       center
     >
       <template>
-        <el-form ref="form" :model="form" label-width="80px">
-          <el-form-item label="打包类型">
-            <el-select v-model="form.type" placeholder="选择打包类型">
-              <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="打包名字" style="width: 70%; padding-right: 2px">
-            <el-input v-model="form.name" style="width: 70%; padding-right: 2px" />
-          </el-form-item>
-          <div v-if="form.type === 'docker'">
-            <el-form-item label="docker 仓库">
-              <el-select v-model="form.dockerRegistry" placeholder="选择 docker 仓库">
-                <el-option
-                  v-for="(item, index) in registryList"
-                  :key="index"
-                  :label="item.label"
-                  :value="item.value"
-                />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="存放位置" style="width: 70%; padding-right: 2px">
-              <el-input v-model="form.targetPath" style="width: 70%; padding-right: 2px" />
-            </el-form-item>
-          </div>
-          <div v-else>
-            <el-form-item label="bin 目录路径" style="width: 70%; padding-right: 2px">
-              <el-input v-model="form.binDirname" style="width: 70%; padding-right: 2px" />
+        <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 === 'staticFiles'">
+                <el-form-item label="构建产物路径" prop="binDirname">
+                  <el-input v-model="form.binDirname" placeholder="例如 /dist" />
+                  <div class="form-tip">源码根目录下的相对路径</div>
+                </el-form-item>
+                <el-form-item label="存储位置" prop="targetPath">
+                  <el-input v-model="form.targetPath" placeholder="例如 registry.cn-chengdu.aliyuncs.com/reghao" />
+                  <div class="form-tip">shell 类型需要使用绝对路径</div>
+                </el-form-item>
+              </div>
+              <div v-else>
+                <el-form-item label="提示">
+                  <el-input value="dockerBuild 类型可能还需要设置 dockerfile 中的参数变量" readonly />
+                </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>
-          </div>
-          <el-form-item>
-            <el-button type="primary" @click="onAddPacker">确定</el-button>
-          </el-form-item>
-        </el-form>
+          </el-form>
+        </div>
       </template>
     </el-dialog>
+    <el-dialog
+        title="添加 docker 仓库"
+        append-to-body
+        :visible.sync="showAddRegistryDialog"
+        center
+    >
+    </el-dialog>
   </el-container>
 </template>
 
@@ -124,16 +166,35 @@ export default {
       totalSize: 0,
       dataList: [],
       // **********************************************************************
+      active: 0,
       showAddDialog: false,
       form: {
         type: '',
         name: '',
-        binDirname: '/',
+        binDirname: '',
         targetPath: '',
         dockerRegistry: ''
       },
       packTypes: [],
-      registryList: []
+      registryList: [],
+      // **********************************************************************
+      showAddRegistryDialog: false,
+      registryForm: {
+        registryUrl: ''
+      }
+    }
+  },
+  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() {
@@ -174,7 +235,7 @@ export default {
         this.$message.error(error.message)
       })
     },
-    onAddPacker() {
+    onAddPacker1() {
       const formData = new FormData()
       formData.append('type', this.form.type)
       formData.append('name', this.form.name)
@@ -190,6 +251,39 @@ export default {
         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('请检查基础配置是否填写完整')
+      }
+    },
+    onAddPacker() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          // 这里发起 API 请求
+          console.log('Final Data:', this.form)
+          this.$message.success('打包配置添加成功!')
+          // 逻辑处理,如关闭弹窗或跳转列表
+        } else {
+          return false
+        }
+      })
+    },
     handleEdit(index, row) {
       this.$confirm('确定要删除 ' + row.name + '?', '提示', {
         confirmButtonText: '确定',
@@ -210,6 +304,9 @@ export default {
           message: '已取消'
         })
       })
+    },
+    goToAddRegistry() {
+      this.showAddRegistryDialog = true
     }
   }
 }

+ 148 - 43
src/views/devops/build/RepoAuth.vue

@@ -62,50 +62,75 @@
 
     <!-- 添加编译器对话框 -->
     <el-dialog
-      title="添加编译器"
+      title="添加仓库认证"
       append-to-body
       :visible.sync="showAddDialog"
       center
     >
-      <template>
-        <el-form ref="form" :model="form" label-width="80px">
-          <el-form-item label="仓库类型">
-            <el-select v-model="form.type" placeholder="选择编译类型">
-              <el-option
-                v-for="(item, index) in repoTypes"
-                :key="index"
-                :label="item.label"
-                :value="item.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="认证类型">
-            <el-select v-model="form.authType" placeholder="选择编译类型">
-              <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="认证名字">
-            <el-input v-model="form.name" style="width: 70%; padding-right: 2px" />
-          </el-form-item>
-          <el-form-item label="用户名" style="width: 70%; padding-right: 2px">
-            <el-input v-model="form.username" style="width: 70%; padding-right: 2px" />
-          </el-form-item>
-          <el-form-item label="密码">
-            <el-input v-model="form.password" style="width: 70%; padding-right: 2px" />
-          </el-form-item>
-<!--          <el-form-item label="私钥" style="width: 70%; padding-right: 2px">
-            <el-input v-model="form.rsaPrikey" type="textarea" autosize style="padding-right: 1px;" />
-          </el-form-item>-->
-          <el-form-item>
-            <el-button type="primary" @click="onAddRepoAuth">确定</el-button>
+      <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>
+            </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>
-      </template>
+      </div>
     </el-dialog>
   </el-container>
 </template>
@@ -129,10 +154,11 @@ export default {
       dataList: [],
       // **********************************************************************
       showAddDialog: false,
+      active: 0,
       form: {
-        type: 'git',
-        name: '',
         authType: 'http',
+        name: '',
+        type: 'git',
         username: '',
         password: '',
         rsaPrikey: ''
@@ -141,6 +167,23 @@ export default {
       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' }]
+      } 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
+    }
+  },
   created() {
     document.title = '仓库认证列表'
     this.getData()
@@ -169,9 +212,9 @@ export default {
     handleShowAdd(index, row) {
       getRepoTypes().then(resp => {
         if (resp.code === 0) {
-          this.showAddDialog = true
           this.repoTypes = resp.data.repoTypes
           this.authTypes = resp.data.authTypes
+          this.showAddDialog = true
         } else {
           this.$message.error(resp.msg)
         }
@@ -179,7 +222,7 @@ export default {
         this.$message.error(error.message)
       })
     },
-    onAddRepoAuth() {
+    onAddRepoAuth1() {
       const formData = new FormData()
       formData.append('type', this.form.type)
       formData.append('name', this.form.name)
@@ -195,6 +238,39 @@ export default {
         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) {
+          // 这里发起 API 请求
+          console.log('Final Data:', this.form)
+          this.$message.success('仓库认证配置添加成功!')
+          // 逻辑处理,如关闭弹窗或跳转列表
+        } else {
+          return false
+        }
+      })
+    },
     handleEdit(index, row) {
       this.$confirm('确定要删除 ' + row.name + '?', '提示', {
         confirmButtonText: '确定',
@@ -220,5 +296,34 @@ export default {
 }
 </script>
 
-<style>
+<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>

+ 63 - 14
src/views/devops/docker/DockerRegistry.vue

@@ -59,27 +59,59 @@
       center
     >
       <template>
-        <el-form ref="form" :model="form" label-width="80px">
-          <el-form-item label="仓库地址">
-            <el-input v-model="form.registryUrl" style="width: 70%; padding-right: 2px" />
-          </el-form-item>
-          <el-form-item label="用户名" style="width: 70%; padding-right: 2px">
-            <el-input v-model="form.username" style="width: 70%; padding-right: 2px" />
+        <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.password" style="width: 70%; padding-right: 2px" />
+
+          <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>
             <el-button type="primary" @click="onAddDockerRegistry">确定</el-button>
           </el-form-item>
         </el-form>
       </template>
     </el-dialog>
+    <el-dialog
+        title="添加仓库认证"
+        append-to-body
+        :visible.sync="showAddRepoAuthDialog"
+        center
+    >
+
+    </el-dialog>
   </el-container>
 </template>
 
 <script>
-import { addDockerRegistry, deleteDockerRegistry, getDockerRegistryList } from '@/api/devops'
+import { addDockerRegistry, deleteDockerRegistry, getDockerRegistryList, getRepoAuthNames } from '@/api/devops'
 
 export default {
   name: 'DockerRegistry',
@@ -97,10 +129,14 @@ export default {
       dataList: [],
       // **********************************************************************
       showAddDialog: false,
+      repoAuthNames: [],
       form: {
-        registryUrl: '',
-        username: '',
-        password: ''
+        repoAuthName: '',
+        registryUrl: ''
+      },
+      // **********************************************************************
+      showAddRepoAuthDialog: false,
+      repoAuthForm: {
       }
     }
   },
@@ -131,12 +167,22 @@ export default {
     },
     handleShowAdd() {
       this.showAddDialog = true
+      getRepoAuthNames().then(resp => {
+        if (resp.code === 0) {
+          this.repoAuthNames = resp.data
+        } else {
+          this.showAddDialog = false
+          this.$message.info(resp.msg)
+        }
+      }).catch(error => {
+        this.showAddDialog = false
+        this.$message.error(error.message)
+      })
     },
     onAddDockerRegistry() {
       const formData = new FormData()
       formData.append('registryUrl', this.form.registryUrl)
-      formData.append('username', this.form.username)
-      formData.append('password', this.form.password)
+      formData.append('repoAuthName', this.form.repoAuthName)
       addDockerRegistry(formData).then(resp => {
         this.$message.info(resp.msg)
         this.getData()
@@ -166,6 +212,9 @@ export default {
           message: '已取消'
         })
       })
+    },
+    handleAddRepoAuth() {
+      this.showAddRepoAuthDialog = true
     }
   }
 }