Pārlūkot izejas kodu

1.新增根证书下载和删除
2.各表单补充字段长度校验
3.去除一些不合适的按钮防抖

wangkun 3 nedēļas atpakaļ
vecāks
revīzija
6120dc60c5

+ 1 - 1
.env.development

@@ -11,7 +11,7 @@ VITE_POSITION = close
 
 VITE_EDITOR = vscode
 
-VITE_ICVP_SERVER = http://10.10.10.88:8080
+VITE_ICVP_SERVER = http://10.10.10.88:9050
 
 VITE_USER_AGENT = "cms"
 

+ 12 - 19
.env.production

@@ -1,25 +1,18 @@
 ENV = 'production'
 
-VITE_CLI_PORT = 8700
-VITE_SERVER_PORT = 8888
+VITE_CLI_PORT = 9020
+
 VITE_BASE_API = /api
-VITE_FILE_API = /api
-#下方修改为你的线上ip
+
 VITE_BASE_PATH = http://10.10.10.30
 
-VITE_QRCODE_BASE_PATH = http://10.10.10.30:9200 # 生成二维码接口调用的地址
-
-VITE_FILE_PATH = http://10.10.10.25:9020/test/
-// 业务端服务
-VITE_SERVICE_PATH = http://10.10.10.30
-VITE_SERVICE_PORT = 9200
-// 业务端WS
-VITE_WEBSOCKET_PATH = ws://10.10.10.30:9200
-// 业务端邀请注册链接
-VITE_INVITE_BASE_PATH = "http://10.10.10.30:8700/#/user/register?code="
-// 学员端登录链接(需要启动学员端项目)
-VITE_STUDENT_LOGIN_PATH = "http://10.10.10.30:8070/#/login"
-// 视频切片统一后缀
-VITE_VIDEO_SUFFIX = "-style.m3u8"
-// 客户端标记
+VITE_SERVER_PORT = 8888
+
+VITE_POSITION = close
+
+VITE_EDITOR = vscode
+
+VITE_ICVP_SERVER = http://10.10.10.30:8080
+
 VITE_USER_AGENT = "cms"
+

+ 2 - 9
public/config.js

@@ -3,14 +3,7 @@ window.APP_CONFIG = {
   // HTTP API
   BASE_API: '/api',              // 给 axios 用(走 nginx)
   // 后端原始地址(如果代码里用到)
-  BASE_PATH: 'http://127.0.0.1:9200',
-  SERVICE_PATH: 'http://127.0.0.1:9200',
-  // WebSocket
-  WEBSOCKET_PATH: 'ws://127.0.0.1:9200/QRCode',
+  BASE_PATH: 'http://10.10.10.30:8888',
+  VITE_ICVP_SERVER: 'http://10.10.10.30:9050',
 
-  QRCODE_BASE_PATH: 'http://127.0.0.1:9200',
-
-  INVITE_BASE_PATH: 'http://127.0.0.1:8080/#/user/register?code=',
-  // 学员端登录链接(需要启动学员端项目)
-  STUDENT_LOGIN_PATH: 'http://127.0.0.1:8070/#/login',
 }

+ 7 - 0
src/api/sysApplication/certificate.js

@@ -94,3 +94,10 @@ export const revokeCertificate = data => {
   })
 }
 
+// 证书删除
+export const deleteCertificate = data => {
+  return service({
+    url: '/v1/certificate/delete?id=' + data,
+    method: 'post'
+  })
+}

+ 1 - 1
src/view/example/upload/upload.vue

@@ -19,7 +19,7 @@
           placeholder="请输入文件名或备注"
         />
         <el-button
-          v-preReClick
+
           icon="search"
           type="primary"
           @click="getTableData"

+ 78 - 14
src/view/sysApplication/pki/certManager/index.vue

@@ -30,14 +30,14 @@
         </el-form-item>
         <el-form-item>
           <el-button
-            v-preReClick
+
             icon="search"
             type="primary"
             @click="onSubmit"
           >查询
           </el-button>
           <el-button
-            v-preReClick
+
             icon="refresh"
             @click="onReset"
           >重置
@@ -49,7 +49,7 @@
     <div class="gva-table-box">
       <div class="gva-btn-list">
         <el-button
-          v-preReClick
+
           icon="plus"
           type="primary"
           @click="addCertDialog()"
@@ -135,14 +135,14 @@
             <!-- 冻结 / 解冻:同一按钮,按状态切换 -->
             <el-button
               v-if="scope.row.status === '1'"
-              v-preReClick
+
               link
               type="primary"
               @click="handleFreeze(scope.row)"
             >冻结</el-button>
             <el-button
               v-else-if="scope.row.status === '3'"
-              v-preReClick
+
               link
               type="primary"
               @click="handleUnfreeze(scope.row)"
@@ -150,7 +150,7 @@
 
             <!-- 续期:有效 / 冻结 / 已过期 可续 -->
             <el-button
-              v-preReClick
+
               link
               type="primary"
               @click="handleRenew(scope.row)"
@@ -159,7 +159,7 @@
             <!-- 吊销:未吊销的都可吊销 -->
             <el-button
               v-if="scope.row.status !== '4'"
-              v-preReClick
+
               link
               type="danger"
               @click="handleRevoke(scope.row)"
@@ -304,6 +304,7 @@
           </el-form-item>
           <el-form-item>
             <el-button
+              v-preReClick
               type="primary"
               @click="submitAddCertForm"
             >提交</el-button>
@@ -415,10 +416,73 @@ const addCertForm = ref({
   isCa: '1',
   certUsage: 'PERSONAL'
 })
+const optionalDnFieldRule = (label) => ({
+  validator: (_rule, value, callback) => {
+    if (value === '' || value === null || value === undefined) {
+      callback()
+      return
+    }
+    if (value.length > 64) {
+      callback(new Error(`证书${label}项长度不能超过64个字符`))
+    } else {
+      callback()
+    }
+  },
+  trigger: 'blur'
+})
 const addCertRules = {
-  certName: [{ required: true, message: '请输入证书名称', trigger: 'blur' }],
-  commonName: [{ required: true, message: '请输入证书CN项', trigger: 'blur' }],
-  country: [{ required: true, message: '请输入证书C项', trigger: 'blur' }],
+  certName: [
+    { required: true, message: '请输入证书名称', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        if (value === '' || value === null || value === undefined) {
+          callback()
+          return
+        }
+        if (value.length > 30) {
+          callback(new Error('证书名称长度不能超过30个字符'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  commonName: [
+    { required: true, message: '请输入证书CN项', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        if (value === '' || value === null || value === undefined) {
+          callback()
+          return
+        }
+        if (value.length > 64) {
+          callback(new Error('证书CN项长度不能超过64个字符'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  country: [
+    { required: true, message: '请输入证书C项', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        const country = typeof value === 'string' ? value.trim() : ''
+        if (!/^[A-Z]{2}$/.test(country)) {
+          callback(new Error('证书C项必须为2位大写国家代码,如 CN'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  organization: [optionalDnFieldRule('O')],
+  organizationalUnit: [optionalDnFieldRule('OU')],
+  state: [optionalDnFieldRule('ST')],
+  locality: [optionalDnFieldRule('L')],
   rootCa: [{ required: true, message: '请选择CA', trigger: 'change' }],
   keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'change' }],
   keySize: [{ required: true, message: '请选择密钥长度', trigger: 'change' }],
@@ -554,7 +618,7 @@ const handleFreeze = async(row) => {
     })
     await freezeCertificate({ id: row.id })
     ElMessage.success('冻结成功')
-    getTableData()
+    await getTableData()
   } catch (err) {
     if (err === 'cancel' || err === 'close') return
     console.error(err)
@@ -571,7 +635,7 @@ const handleUnfreeze = async(row) => {
     })
     await unfreezeCertificate({ id: row.id })
     ElMessage.success('解冻成功')
-    getTableData()
+    await getTableData()
   } catch (err) {
     if (err === 'cancel' || err === 'close') return
     console.error(err)
@@ -599,7 +663,7 @@ const handleRenew = async(row) => {
     const res = await renewCertificate({ id: row.id, validityDays: Number(value) })
     if (res.code === 100000) {
       ElMessage.success('续期成功')
-      getTableData()
+      await getTableData()
     }
   } catch (err) {
     if (err === 'cancel' || err === 'close') return
@@ -623,7 +687,7 @@ const handleRevoke = async(row) => {
     )
     await revokeCertificate({ id: row.id, reason: value || '' })
     ElMessage.success('吊销成功')
-    getTableData()
+    await getTableData()
   } catch (err) {
     if (err === 'cancel' || err === 'close') return
     console.error(err)

+ 371 - 61
src/view/sysApplication/pki/rootCaManager/index.vue

@@ -30,14 +30,12 @@
         </el-form-item>
         <el-form-item>
           <el-button
-            v-preReClick
             icon="search"
             type="primary"
             @click="onSubmit"
           >查询
           </el-button>
           <el-button
-            v-preReClick
             icon="refresh"
             @click="onReset"
           >重置
@@ -50,14 +48,12 @@
 
       <div class="gva-btn-list">
         <el-button
-          v-preReClick
           icon="plus"
           type="primary"
           @click="issueCertDialog()"
         >签发根证书
         </el-button>
         <el-button
-          v-preReClick
           icon="plus"
           type="primary"
           @click="addCertDialog()"
@@ -81,7 +77,7 @@
           header-align="center"
           label="证书名称"
           prop="certName"
-          width="300"
+          width="280"
         />
         <el-table-column
           label="证书算法"
@@ -118,37 +114,66 @@
         <el-table-column
           fixed="right"
           label="操作"
+          width="260"
         >
-<!--          <template #default="scope">
-            <el-button
+          <template #default="scope">
+            <!-- 下载(保留之前的下拉) -->
+            <el-dropdown
+              trigger="hover"
+              class="download-dropdown"
+              @command="(command) => handleDownload(scope.row, command)"
+            >
+              <el-button
+                v-preReClick
+                link
+                type="primary"
+              >下载</el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="pem">下载 PEM 格式</el-dropdown-item>
+                  <el-dropdown-item command="p12">下载 P12 格式</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+
+            <!-- 冻结 / 解冻:同一按钮,按状态切换 -->
+<!--            <el-button
+              v-if="scope.row.status === '1'"
               v-preReClick
               link
               type="primary"
-              @click="openEditDialog(scope.row)"
-            >下载
-            </el-button>
+              @click="handleFreeze(scope.row)"
+            >冻结</el-button>
             <el-button
+              v-else-if="scope.row.status === '3'"
               v-preReClick
               link
               type="primary"
-              @click="updateStatus(scope.row, '2')"
-            >冻结
-            </el-button>
+              @click="handleUnfreeze(scope.row)"
+            >解冻</el-button>
+
+            &lt;!&ndash; 续期:有效 / 冻结 / 已过期 可续 &ndash;&gt;
             <el-button
               v-preReClick
               link
               type="primary"
-              @click="updateStatus(scope.row, '1')"
-            >延期
-            </el-button>
+              @click="handleRenew(scope.row)"
+            >续期</el-button>
+
+            &lt;!&ndash; 吊销:未吊销的都可吊销 &ndash;&gt;
             <el-button
+              v-if="scope.row.status !== '4'"
               v-preReClick
               link
-              type="primary"
-              @click="updateStatus(scope.row, '1')"
-            >吊销
-            </el-button>
-          </template>-->
+              type="danger"
+              @click="handleRevoke(scope.row)"
+            >吊销</el-button>-->
+            <el-button
+              link
+              type="danger"
+              @click="handleDelete(scope.row)"
+            >删除</el-button>
+          </template>
         </el-table-column>
       </el-table>
 
@@ -159,11 +184,16 @@
         @close="handleClose"
       >
         <el-form
+          ref="uploadFormRef"
           :model="uploadForm"
+          :rules="uploadRules"
           label-width="100px"
         >
           <!-- 证书名称(可选) -->
-          <el-form-item label="证书名称">
+          <el-form-item
+            label="证书名称"
+            prop="certName"
+          >
             <el-input
               v-model="uploadForm.certName"
               placeholder="请输入证书名称(可选)"
@@ -199,6 +229,7 @@
         <template #footer>
           <el-button @click="handleClose">取消</el-button>
           <el-button
+            v-preReClick
             type="primary"
             :loading="uploading"
             @click="handleSubmit"
@@ -224,7 +255,10 @@
             label="证书算法"
             prop="keyAlgorithm"
           >
-            <el-select v-model="addCertForm.keyAlgorithm" placeholder="请选择证书算法">
+            <el-select
+              v-model="addCertForm.keyAlgorithm"
+              placeholder="请选择证书算法"
+            >
               <el-option
                 v-for="item in keyAlgoOptions"
                 :key="item.value"
@@ -238,52 +272,82 @@
             label="密钥长度"
             prop="keySize"
           >
-            <el-select v-model="addCertForm.keySize" placeholder="请选择密钥长度">
-              <el-option label="2048" value="2048" />
-              <el-option label="4096" value="4096" />
+            <el-select
+              v-model="addCertForm.keySize"
+              placeholder="请选择密钥长度"
+            >
+              <el-option
+                label="2048"
+                value="2048"
+              />
+              <el-option
+                label="4096"
+                value="4096"
+              />
             </el-select>
           </el-form-item>
           <el-form-item
             label="证书名称"
             prop="certName"
           >
-            <el-input v-model="addCertForm.certName" placeholder="请输入证书名称" />
+            <el-input
+              v-model="addCertForm.certName"
+              placeholder="请输入证书名称"
+            />
           </el-form-item>
           <el-form-item
             label="公用名"
             prop="commonName"
           >
-            <el-input v-model="addCertForm.commonName" placeholder="请输入证书CN项" />
+            <el-input
+              v-model="addCertForm.commonName"
+              placeholder="请输入证书CN项"
+            />
           </el-form-item>
           <el-form-item
             label="国家"
             prop="country"
           >
-            <el-input v-model="addCertForm.country" placeholder="请输入证书C项" />
+            <el-input
+              v-model="addCertForm.country"
+              placeholder="请输入证书C项"
+            />
           </el-form-item>
           <el-form-item
             label="组织"
             prop="organization"
           >
-            <el-input v-model="addCertForm.organization" placeholder="请输入证书O项" />
+            <el-input
+              v-model="addCertForm.organization"
+              placeholder="请输入证书O项"
+            />
           </el-form-item>
           <el-form-item
             label="组织单位"
             prop="organizationUnit"
           >
-            <el-input v-model="addCertForm.organizationUnit" placeholder="请输入证书OU项" />
+            <el-input
+              v-model="addCertForm.organizationUnit"
+              placeholder="请输入证书OU项"
+            />
           </el-form-item>
           <el-form-item
             label="省份"
             prop="state"
           >
-            <el-input v-model="addCertForm.state" placeholder="请输入证书ST项" />
+            <el-input
+              v-model="addCertForm.state"
+              placeholder="请输入证书ST项"
+            />
           </el-form-item>
           <el-form-item
             label="城市"
             prop="locality"
           >
-            <el-input v-model="addCertForm.locality" placeholder="请输入证书L项" />
+            <el-input
+              v-model="addCertForm.locality"
+              placeholder="请输入证书L项"
+            />
           </el-form-item>
           <el-form-item
             label="证书有效期"
@@ -299,7 +363,11 @@
 
         <template #footer>
           <el-button @click="issueCertDialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="submitAddCertForm">提交</el-button>
+          <el-button
+            v-preReClick
+            type="primary"
+            @click="submitAddCertForm"
+          >提交</el-button>
         </template>
       </el-dialog>
 
@@ -321,8 +389,14 @@
 </template>
 <script setup>
 import { ref } from 'vue'
-import { addRootCa, listCertificates, uploadRoot } from '@/api/sysApplication/certificate'
-import { ElMessage } from 'element-plus'
+import {
+  addRootCa, deleteCertificate, downloadPemCombined, downloadPkcs12, freezeCertificate,
+  listCertificates,
+  renewCertificate,
+  revokeCertificate, unfreezeCertificate,
+  uploadRoot,
+} from '@/api/sysApplication/certificate'
+import { ElMessage, ElMessageBox } from 'element-plus'
 const searchInfo = ref({})
 const addCertFormRef = ref(null)
 const elSearchFormRef = ref(null)
@@ -389,10 +463,83 @@ const addCertForm = ref({
   certUsage: 'ROOT_CA',
   remark: ''
 })
+const uploadFormRef = ref(null)
+const uploadRules = {
+  certName: [{
+    validator: (_rule, value, callback) => {
+      if (value && value.length > 30) {
+        callback(new Error('证书名称长度不能超过30个字符'))
+      } else {
+        callback()
+      }
+    },
+    trigger: 'blur'
+  }]
+}
+const optionalDnFieldRule = (label) => ({
+  validator: (_rule, value, callback) => {
+    if (value === '' || value === null || value === undefined) {
+      callback()
+      return
+    }
+    if (value.length > 64) {
+      callback(new Error(`证书${label}项长度不能超过64个字符`))
+    } else {
+      callback()
+    }
+  },
+  trigger: 'blur'
+})
 const addCertRules = {
-  certName: [{ required: true, message: '请输入证书名称', trigger: 'blur' }],
-  commonName: [{ required: true, message: '请输入证书CN项', trigger: 'blur' }],
-  country: [{ required: true, message: '请输入证书C项', trigger: 'blur' }],
+  certName: [{ required: true, message: '请输入证书名称', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        if (value === '' || value === null || value === undefined) {
+          callback()
+          return
+        }
+        if (value.length > 30) {
+          callback(new Error('证书名称长度不能超过30个字符'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  commonName: [{ required: true, message: '请输入证书CN项', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        if (value === '' || value === null || value === undefined) {
+          callback()
+          return
+        }
+        if (value.length > 64) {
+          callback(new Error('证书CN项长度不能超过64个字符'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  organization: [optionalDnFieldRule('O')],
+  organizationUnit: [optionalDnFieldRule('OU')],
+  state: [optionalDnFieldRule('ST')],
+  locality: [optionalDnFieldRule('L')],
+  country: [{ required: true, message: '请输入证书C项', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        const country = typeof value === 'string' ? value.trim() : ''
+        if (!/^[A-Z]{2}$/.test(country)) {
+          callback(new Error('证书C项必须为2位大写国家代码,如 CN'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
   keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'change' }],
   keySize: [{ required: true, message: '请选择密钥长度', trigger: 'change' }],
   validityDays: [
@@ -481,29 +628,35 @@ const handleExceed = () => {
 
 // 提交
 const handleSubmit = async() => {
-  if (!uploadForm.value.file) {
-    ElMessage.warning('请先选择证书文件')
-    return
-  }
+  uploadFormRef.value?.validate(async valid => {
+    if (!valid) return
 
-  const formData = new FormData()
-  formData.append('file', uploadForm.value.file)
-  if (uploadForm.value.certName) {
-    formData.append('certName', uploadForm.value.certName)
-  }
+    if (!uploadForm.value.file) {
+      ElMessage.warning('请先选择证书文件')
+      return
+    }
 
-  uploading.value = true
-  try {
-    const res = uploadRoot(formData)
-    // 根据你的 ResponseDTO 结构判断
-      ElMessage.success("上传根证书成功")
-      addCertDialogVisible.value = false
-      await getTableData()
-  } catch (err) {
-    ElMessage.error(err.message)
-  } finally {
-    uploading.value = false
-  }
+    const formData = new FormData()
+    formData.append('file', uploadForm.value.file)
+    if (uploadForm.value.certName) {
+      formData.append('certName', uploadForm.value.certName)
+    }
+
+    uploading.value = true
+    try {
+      const res = uploadRoot(formData)
+      // 根据你的 ResponseDTO 结构判断
+      if (res.code === 100000) {
+        ElMessage.success('上传根证书成功')
+        addCertDialogVisible.value = false
+        await getTableData()
+      }
+    } catch (err) {
+      ElMessage.error(err.message)
+    } finally {
+      uploading.value = false
+    }
+  })
 }
 const fileList = ref([])
 const uploadRef = ref(null)
@@ -516,8 +669,165 @@ const handleClose = () => {
   fileList.value = []
   uploadRef.value?.clearFiles()
 }
+
+/**
+ * 触发下载
+ * @param {Object} row - 表格行数据
+ * @param {String} format - 'pem' | 'p12'
+ */
+const handleDownload = async(row, format) => {
+  try {
+    let res
+    if (format === 'pem') {
+      res = await downloadPemCombined({ id: row.id })
+      saveBlob(res, `certificate-${row.id}.pem`)
+    } else if (format === 'p12') {
+      // p12 需要密码,弹框让用户输入
+      const { value: password } = await ElMessageBox.prompt(
+        '请输入 P12 文件的保护密码',
+        '下载 P12',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          inputType: 'password',
+          inputValidator: (val) => {
+            if (!val) return '密码不能为空'
+            return true
+          }
+        }
+      )
+      res = await downloadPkcs12({ id: row.id, password })
+      saveBlob(res, `certificate-${row.id}.p12`)
+    }
+  } catch (err) {
+    // 用户取消弹框
+    if (err === 'cancel' || err === 'close') return
+    console.error('下载失败', err)
+    ElMessage.error('下载失败')
+  }
+}
+
+// 冻结
+const handleFreeze = async(row) => {
+  try {
+    await ElMessageBox.confirm(`确认冻结证书「${row.certSerialNumber}」吗?`, '冻结确认', {
+      type: 'warning',
+      confirmButtonText: '确定',
+      cancelButtonText: '取消'
+    })
+    await freezeCertificate({ id: row.id })
+    ElMessage.success('冻结成功')
+    await getTableData()
+  } catch (err) {
+    if (err === 'cancel' || err === 'close') return
+    console.error(err)
+  }
+}
+
+// 解冻
+const handleUnfreeze = async(row) => {
+  try {
+    await ElMessageBox.confirm(`确认解冻证书「${row.certSerialNumber}」吗?`, '解冻确认', {
+      type: 'warning',
+      confirmButtonText: '确定',
+      cancelButtonText: '取消'
+    })
+    await unfreezeCertificate({ id: row.id })
+    ElMessage.success('解冻成功')
+    await getTableData()
+  } catch (err) {
+    if (err === 'cancel' || err === 'close') return
+    console.error(err)
+  }
+}
+
+// 续期
+const handleRenew = async(row) => {
+  try {
+    const { value } = await ElMessageBox.prompt(
+      '请输入续期天数',
+      '证书续期',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        inputValue: 365, // 默认值,对应后端 default 365
+        inputType: 'number',
+        inputValidator: (val) => {
+          const num = Number(val)
+          if (!val || isNaN(num) || num < 1) return '请输入大于等于 1 的天数'
+          return true
+        }
+      }
+    )
+    const res = await renewCertificate({ id: row.id, validityDays: Number(value) })
+    if (res.code === 100000) {
+      ElMessage.success('续期成功')
+      await getTableData()
+    }
+  } catch (err) {
+    if (err === 'cancel' || err === 'close') return
+    console.error(err)
+  }
+}
+
+// 吊销
+const handleRevoke = async(row) => {
+  try {
+    const { value } = await ElMessageBox.prompt(
+      '请输入吊销原因(可选)',
+      '证书吊销',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        inputType: 'textarea',
+        inputPlaceholder: '请输入吊销原因',
+        inputValidator: () => true // reason 非必填,不校验
+      }
+    )
+    await revokeCertificate({ id: row.id, reason: value || '' })
+    ElMessage.success('吊销成功')
+    await getTableData()
+  } catch (err) {
+    if (err === 'cancel' || err === 'close') return
+    console.error(err)
+  }
+}
+const handleDelete = async(row) => {
+  try {
+    await ElMessageBox.confirm(`确认删除证书「${row.certSerialNumber}」吗?`, '删除确认', {
+      type: 'warning',
+      confirmButtonText: '确定',
+      cancelButtonText: '取消'
+    })
+    await deleteCertificate(row.id)
+    ElMessage.success('删除成功')
+    await getTableData()
+  } catch (err) {
+    if (err === 'cancel' || err === 'close') return
+    console.error(err)
+  }
+}
+/**
+ * 通用 blob 保存工具
+ */
+const saveBlob = (res, filename) => {
+  // 注意:如果 service 拦截器已经处理过 response.data,res 可能直接就是 blob
+  const blobData = res.data instanceof Blob ? res.data : new Blob([res.data])
+  const url = window.URL.createObjectURL(blobData)
+  const link = document.createElement('a')
+  link.href = url
+  link.download = filename
+  document.body.appendChild(link)
+  link.click()
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}
 </script>
 
 <style scoped lang="scss">
-
+.download-dropdown {
+  vertical-align: middle;
+  line-height: 1;
+  margin-right: 12px;
+}
 </style>

+ 30 - 7
src/view/sysApplication/signature/picManager/index.vue

@@ -16,14 +16,14 @@
         </el-form-item>
         <el-form-item>
           <el-button
-            v-preReClick
+
             icon="search"
             type="primary"
             @click="onSubmit"
           >查询
           </el-button>
           <el-button
-            v-preReClick
+
             icon="refresh"
             @click="onReset"
           >重置
@@ -35,7 +35,7 @@
     <div class="gva-table-box">
       <div class="gva-btn-list">
         <el-button
-          v-preReClick
+
           icon="plus"
           type="primary"
           @click="addPicDialog"
@@ -117,6 +117,7 @@
             <el-input
               v-model="makeSealForm.name"
               placeholder="请输入印模别名"
+
             />
           </el-form-item>
           <!-- 印章类型 -->
@@ -135,6 +136,7 @@
             <el-input
               v-model="makeSealForm.message"
               placeholder="如:XX有限公司"
+
             />
           </el-form-item>
           <!-- 中心印模 -->
@@ -146,6 +148,7 @@
             <el-input
               v-model="makeSealForm.centerName"
               placeholder="如:合同专用章"
+
             />
           </el-form-item>
           <!-- 方章 -->
@@ -162,6 +165,7 @@
           <!-- 操作 -->
           <el-form-item>
             <el-button
+              v-preReClick
               type="primary"
               @click="generateSeal"
             >
@@ -274,11 +278,30 @@ const makeSealForm = ref({
 })
 
 // 动态校验规则:方章不需要中心文字
+const maxLengthRule = (label, maxLength) => ({
+  validator: (_rule, value, callback) => {
+    if (value && value.length > maxLength) {
+      callback(new Error(`${label}不能超过${maxLength}个字符`))
+    } else {
+      callback()
+    }
+  },
+  trigger: 'blur'
+})
 const makeSealRules = {
-  name: [{ required: true, message: '请输入印模别名', trigger: 'blur' }],
+  name: [
+    { required: true, message: '请输入印模别名', trigger: 'blur' },
+    maxLengthRule('印模别名', 30)
+  ],
   type: [{ required: true, message: '请选择印章类型', trigger: 'change' }],
-  message: [{ required: true, message: '请输入印章信息', trigger: 'blur' }],
-  centerName: [{ required: true, message: '请输入印章中心名称', trigger: 'blur' }],
+  message: [
+    { required: true, message: '请输入印章信息', trigger: 'blur' },
+    maxLengthRule('印模文字', 20)
+  ],
+  centerName: [
+    { required: true, message: '请输入印章中心名称', trigger: 'blur' },
+    maxLengthRule('中心文字', 20)
+  ],
   sideLength: [{ required: true, message: '请输入印章边长', trigger: 'blur' }],
 }
 
@@ -371,4 +394,4 @@ const handleDelete = async (row) => {
 </script>
 
 <style scoped lang="scss">
-</style>
+</style>

+ 1 - 4
src/view/sysApplication/signature/sealManager/index.vue

@@ -16,13 +16,11 @@
         </el-form-item>
         <el-form-item>
           <el-button
-            v-preReClick
             type="primary"
             icon="search"
             @click="onSubmit"
           >查询</el-button>
           <el-button
-            v-preReClick
             icon="refresh"
             @click="onReset"
           >重置</el-button>
@@ -33,7 +31,6 @@
     <div class="gva-table-box">
       <div class="gva-btn-list">
         <el-button
-          v-preReClick
           type="primary"
           icon="plus"
           @click="addSealDialog"
@@ -88,7 +85,6 @@
         >
           <template #default="{ row }">
             <el-button
-              v-preReClick
               link
               type="primary"
               @click="openDetail(row)"
@@ -209,6 +205,7 @@
         <template #footer>
           <el-button @click="addSealDialogVisible = false">取消</el-button>
           <el-button
+            v-preReClick
             type="primary"
             :loading="createLoading"
             @click="submitCreate"