ソースを参照

feat:1.新增数字证书管理
2.新增应用管理
3。新增存证管理

wangkun 1 日 前
親
コミット
a9ef2799c2

+ 51 - 0
src/api/sysApplication/applicationCredentials.js

@@ -0,0 +1,51 @@
+import service from '@/utils/request'
+
+// 应用凭证列表(apiSecret 已脱敏)
+export const getApplicationCredentialList = data => {
+  return service({
+    url: '/v1/appCredentials/list',
+    method: 'post',
+    data
+  })
+}
+
+// 创建应用凭证(标识/密钥/算法由服务端生成)
+export const createApplicationCredential = data => {
+  return service({
+    url: '/v1/appCredentials/create',
+    method: 'post',
+    data
+  })
+}
+
+// 删除应用凭证
+export const deleteApplicationCredential = id => {
+  return service({
+    url: `/v1/appCredentials/delete/${id}`,
+    method: 'post'
+  })
+}
+
+// 重置密钥(重新随机生成 apiSecret)
+export const resetApplicationSecret = id => {
+  return service({
+    url: `/v1/appCredentials/resetSecret/${id}`,
+    method: 'post'
+  })
+}
+
+// 查询 apiSecret 明文
+export const getApplicationSecret = id => {
+  return service({
+    url: `/v1/appCredentials/getSecret/${id}`,
+    method: 'post'
+  })
+}
+
+// 启用/禁用:1-启用,2-禁用
+export const changeApplicationStatus = (id, status) => {
+  return service({
+    url: `/v1/appCredentials/changeStatus/${id}/${status}`,
+    method: 'post'
+  })
+}

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

@@ -0,0 +1,103 @@
+import service from '@/utils/request'
+
+export const listCertificates = data => {
+  return service({
+    url: '/v1/certificate/list',
+    method: 'post',
+    data
+  })
+}
+
+export const addRootCa = data => {
+  return service({
+    url: '/v1/certificate/issueRootCertificate',
+    method: 'post',
+    data
+  })
+}
+export const addCertificate = data => {
+  return service({
+    url: '/v1/certificate/issuePersonalCertificate',
+    method: 'post',
+    data
+  })
+}
+
+export const uploadRoot = data => {
+  return service({
+    url: '/v1/certificate/uploadRoot',
+    method: 'post',
+    data
+  })
+}
+/**
+ * 下载合并的 PEM(cert + private-key)
+ * @param {Object} data - { id: xxx }
+ */
+export const downloadPemCombined = data => {
+  return service({
+    url: '/v1/certificate/download/pem-combined',
+    method: 'post',
+    data,
+    responseType: 'blob'
+  })
+}
+
+/**
+ * 下载 PKCS#12 格式(需要密码)
+ * @param {Object} data - { id: xxx, password: xxx }
+ */
+export const downloadPkcs12 = data => {
+  return service({
+    url: '/v1/certificate/download/pkcs12',
+    method: 'post',
+    data,
+    responseType: 'blob'
+  })
+}
+
+// api/certificate.js
+
+// 证书续期
+export const renewCertificate = data => {
+  return service({
+    url: '/v1/certificate/renew',
+    method: 'post',
+    data
+  })
+}
+
+// 证书冻结
+export const freezeCertificate = data => {
+  return service({
+    url: '/v1/certificate/freeze',
+    method: 'post',
+    data
+  })
+}
+
+// 证书解冻
+export const unfreezeCertificate = data => {
+  return service({
+    url: '/v1/certificate/unfreeze',
+    method: 'post',
+    data
+  })
+}
+
+// 证书吊销
+export const revokeCertificate = data => {
+  return service({
+    url: '/v1/certificate/revoke',
+    method: 'post',
+    data
+  })
+}
+
+// 证书删除
+export const deleteCertificate = data => {
+  return service({
+    url: '/v1/certificate/delete?id=' + data,
+    method: 'post'
+  })
+}

+ 40 - 0
src/api/sysApplication/evidence.js

@@ -0,0 +1,40 @@
+import service from '@/utils/request'
+
+// 存证分页列表
+export const getEvidenceList = data => {
+  return service({
+    url: '/v1/evidence/list',
+    method: 'post',
+    data
+  })
+}
+
+// 按存证标识查询凭证数据(签名/HMAC/摘要/时间戳/原文引用)
+export const getEvidenceDataByIden = data => {
+  return service({
+    url: '/v1/evidence/getEvidenceDataByIden',
+    method: 'post',
+    data
+  })
+}
+
+// 文本存证核验
+export const textEvidenceVerify = data => {
+  return service({
+    url: '/v1/evidence/textEvidenceVerify',
+    method: 'post',
+    data
+  })
+}
+
+// 文件存证核验(multipart)
+export const fileEvidenceVerify = data => {
+  return service({
+    url: '/v1/evidence/fileEvidenceVerify',
+    method: 'post',
+    data,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}

+ 5 - 0
src/utils/request.js

@@ -103,6 +103,11 @@ service.interceptors.response.use(
       closeLoading()
     }
 
+    // 文件下载(blob)直接返回完整响应,交给调用方保存
+    if (response.config.responseType === 'blob') {
+      return response
+    }
+
     if (response.headers['new-token']) {
       userStore.setToken(response.headers['new-token'])
     }

+ 632 - 0
src/view/sysApplication/evidence/index.vue

@@ -0,0 +1,632 @@
+<template>
+  <div>
+    <warning-bar title="注:存证数据一旦生成,将无法篡改和伪造,请妥善管理。" />
+    <div class="gva-table-box">
+      <!-- 查询区域 -->
+      <div class="gva-btn-list">
+        <el-input
+          v-model="queryParams.evidenceIden"
+          placeholder="存证标识"
+          style="width: 180px; margin-right: 10px"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+        <el-input
+          v-model="queryParams.credIden"
+          placeholder="应用标识"
+          style="width: 180px; margin-right: 10px"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+        <el-select
+          v-model="queryParams.type"
+          placeholder="存证类型"
+          style="width: 140px; margin-right: 10px"
+          clearable
+        >
+          <el-option label="文本存证" :value="1" />
+          <el-option label="文件存证" :value="2" />
+        </el-select>
+        <el-button type="primary" @click="handleQuery">查询</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </div>
+
+      <!-- 数据表格 -->
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        row-key="id"
+      >
+        <el-table-column
+          align="center"
+          label="存证编号"
+          width="100"
+          prop="id"
+        />
+        <el-table-column
+          align="center"
+          label="存证标识"
+          min-width="300"
+          prop="evidenceIden"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          align="center"
+          label="应用标识"
+          min-width="300"
+          prop="credIden"
+          show-overflow-tooltip
+        >
+          <template #default="scope">
+            {{ scope.row.credIden || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="存证类型"
+          width="120"
+        >
+          <template #default="scope">
+            <el-tag :type="Number(scope.row.type) === 1 ? 'primary' : 'warning'">
+              {{ Number(scope.row.type) === 1 ? '文本存证' : '文件存证' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="业务操作时间"
+          min-width="180"
+        >
+          <template #default="scope">
+            {{ formatDate(scope.row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="操作"
+          min-width="220"
+          fixed="right"
+        >
+          <template #default="scope">
+            <el-button
+              type="primary"
+              link
+              icon="circle-check"
+              @click="handleVerify(scope.row)"
+            >有效性验证</el-button>
+            <el-button
+              type="primary"
+              link
+              icon="search"
+              @click="handleExtractEvidence(scope.row)"
+            >提取证据</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="gva-pagination">
+        <el-pagination
+          v-model:current-page="page"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @current-change="handlePageChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </div>
+
+    <!-- 提取证据弹窗 -->
+    <el-dialog
+      v-model="evidenceDialog"
+      title="证据提取详情"
+      :close-on-click-modal="false"
+      width="650px"
+    >
+      <div style="max-height: 60vh; overflow: auto; padding: 0 12px">
+        <el-form label-width="110px">
+          <el-form-item label="存证标识">
+            <el-input
+              :model-value="evidenceDetail.evidenceIden"
+              readonly
+            />
+          </el-form-item>
+          <el-form-item label="应用标识">
+            <el-input
+              :model-value="evidenceDetail.credIden || '-'"
+              readonly
+            />
+          </el-form-item>
+          <el-form-item label="存证类型">
+            <el-tag :type="Number(evidenceDetail.type) === 1 ? 'primary' : 'warning'">
+              {{ Number(evidenceDetail.type) === 1 ? '文本存证' : '文件存证' }}
+            </el-tag>
+          </el-form-item>
+          <el-form-item
+            v-if="Number(evidenceDetail.type) === 1"
+            label="原文"
+          >
+            <el-input
+              type="textarea"
+              :rows="4"
+              :model-value="evidenceDetail.originalText"
+              readonly
+            />
+          </el-form-item>
+          <el-form-item
+            v-else
+            label="文件地址"
+          >
+            <div style="width: 100%">
+              <el-input
+                :model-value="evidenceDetail.originalUrl"
+                readonly
+              />
+              <el-link
+                v-if="evidenceDetail.originalUrl"
+                type="primary"
+                :href="evidenceDetail.originalUrl"
+                target="_blank"
+                style="margin-top: 6px"
+              >打开文件</el-link>
+            </div>
+          </el-form-item>
+          <el-form-item label="hash值">
+            <el-input
+              type="textarea"
+              :rows="3"
+              :model-value="evidenceDetail.hashText"
+              readonly
+            />
+          </el-form-item>
+          <el-form-item label="数字签名值">
+            <el-input
+              type="textarea"
+              :rows="2"
+              :model-value="evidenceDetail.signature"
+              readonly
+            />
+          </el-form-item>
+          <el-form-item label="HMAC摘要值">
+            <el-input
+              type="textarea"
+              :rows="2"
+              :model-value="evidenceDetail.hmac"
+              readonly
+            />
+          </el-form-item>
+          <el-form-item label="时间戳值">
+            <el-input
+              :model-value="evidenceDetail.timestamp"
+              readonly
+            />
+          </el-form-item>
+        </el-form>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="evidenceDialog = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 有效性验证结果弹窗 -->
+    <el-dialog
+      v-model="verifyDialog"
+      title="有效性验证结果"
+      :close-on-click-modal="false"
+      width="500px"
+    >
+      <div style="padding: 20px 0">
+        <el-result
+          :icon="verifyResult.valid ? 'success' : 'error'"
+          :title="verifyResult.valid ? '本条存证数据记录有效' : '本条存证数据记录无效'"
+          :sub-title="verifyResult.valid ? '所有验证项均通过' : '存在未通过的验证项,请检查'"
+        >
+          <template #extra>
+            <div class="verify-detail">
+              <div class="verify-item">
+                <span>签名验签:</span>
+                <el-tag :type="verifyResult.signVerify ? 'success' : 'danger'">
+                  {{ verifyResult.signVerify ? '通过' : '未通过' }}
+                </el-tag>
+              </div>
+              <div class="verify-item">
+                <span>凭证 HMAC:</span>
+                <el-tag :type="verifyResult.hmacCheck ? 'success' : 'danger'">
+                  {{ verifyResult.hmacCheck ? '通过' : '未通过' }}
+                </el-tag>
+              </div>
+              <div class="verify-item">
+                <span>内容完整性:</span>
+                <el-tag :type="verifyResult.integrityCheck ? 'success' : 'danger'">
+                  {{ verifyResult.integrityCheck ? '通过' : '未通过' }}
+                </el-tag>
+              </div>
+            </div>
+          </template>
+        </el-result>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="verifyDialog = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 文本存证验证输入弹窗 -->
+    <el-dialog
+      v-model="textVerifyDialog"
+      title="文本存证验证"
+      :close-on-click-modal="false"
+      width="520px"
+    >
+      <el-form
+        :model="textVerifyForm"
+        label-width="110px"
+      >
+        <el-form-item label="应用标识">
+          <el-input
+            v-model="textVerifyForm.credIden"
+            readonly
+          />
+        </el-form-item>
+        <el-form-item label="存证标识">
+          <el-input
+            v-model="textVerifyForm.evidenceIden"
+            readonly
+          />
+        </el-form-item>
+        <el-form-item label="HMAC">
+          <el-input
+            v-model="textVerifyForm.hmac"
+            readonly
+          />
+        </el-form-item>
+        <el-form-item label="存证原文">
+          <el-input
+            v-model="textVerifyForm.text"
+            type="textarea"
+            rows="5"
+            placeholder="请输入签发存证时的原始文本"
+          />
+          <div class="form-tip">默认已带出存证原文;如实际原文与签发时不一致,请修改后再验证。</div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="textVerifyDialog = false">取消</el-button>
+        <el-button
+          type="primary"
+          :loading="verifyLoading"
+          @click="doTextVerify"
+        >开始验证</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 文件存证验证弹窗 -->
+    <el-dialog
+      v-model="fileVerifyDialog"
+      title="文件存证验证"
+      :close-on-click-modal="false"
+      width="520px"
+    >
+      <el-form
+        :model="fileVerifyForm"
+        label-width="110px"
+      >
+        <el-form-item label="应用标识">
+          <el-input
+            v-model="fileVerifyForm.credIden"
+            readonly
+          />
+        </el-form-item>
+        <el-form-item label="存证标识">
+          <el-input
+            v-model="fileVerifyForm.evidenceIden"
+            readonly
+          />
+        </el-form-item>
+        <el-form-item label="HMAC">
+          <el-input
+            v-model="fileVerifyForm.hmac"
+            readonly
+          />
+        </el-form-item>
+        <el-form-item label="原始文件">
+          <el-upload
+            ref="uploadRef"
+            :auto-upload="false"
+            :limit="1"
+            :on-change="handleFileChange"
+            :on-remove="handleFileRemove"
+            accept="*"
+          >
+            <el-button type="primary">选择文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">请上传存证时使用的原始文件</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="fileVerifyDialog = false">取消</el-button>
+        <el-button
+          type="primary"
+          :loading="verifyLoading"
+          @click="doFileVerify"
+        >开始验证</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { nextTick, onMounted, reactive, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import WarningBar from '@/components/warningBar/warningBar.vue'
+import {
+  fileEvidenceVerify,
+  getEvidenceDataByIden,
+  getEvidenceList,
+  textEvidenceVerify
+} from '@/api/sysApplication/evidence'
+import { formatDate } from '@/utils/format'
+
+defineOptions({
+  name: 'EvidenceManagement'
+})
+
+// 查询与分页参数
+const queryParams = reactive({
+  evidenceIden: '',
+  credIden: '',
+  type: undefined
+})
+
+const page = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const tableData = ref([])
+const loading = ref(false)
+
+// 获取列表数据
+const fetchList = async() => {
+  loading.value = true
+  try {
+    const res = await getEvidenceList({
+      pageNum: page.value,
+      pageSize: pageSize.value,
+      evidenceIden: queryParams.evidenceIden || undefined,
+      credIden: queryParams.credIden || undefined,
+      type: queryParams.type || undefined
+    })
+    if (res?.code === 100000) {
+      tableData.value = res.data?.list || []
+      total.value = res.data?.total || 0
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+// 查询
+const handleQuery = () => {
+  page.value = 1
+  fetchList()
+}
+
+// 重置
+const handleReset = () => {
+  queryParams.evidenceIden = ''
+  queryParams.credIden = ''
+  queryParams.type = undefined
+  page.value = 1
+  fetchList()
+}
+
+// 分页
+const handlePageChange = val => {
+  page.value = val
+  fetchList()
+}
+
+const handleSizeChange = val => {
+  pageSize.value = val
+  page.value = 1
+  fetchList()
+}
+
+// 提取证据弹窗
+const evidenceDialog = ref(false)
+const evidenceDetail = ref({
+  evidenceIden: '',
+  credIden: '',
+  type: null,
+  hashText: '',
+  signature: '',
+  hmac: '',
+  timestamp: '',
+  originalText: '',
+  originalUrl: ''
+})
+
+const handleExtractEvidence = async row => {
+  const res = await getEvidenceDataByIden({ evidenceIden: row.evidenceIden })
+  if (res?.code === 100000) {
+    const data = res.data || {}
+    evidenceDetail.value = {
+      evidenceIden: data.evidenceIden || '',
+      credIden: data.credIden || '',
+      type: data.type,
+      hashText: data.hashText || '',
+      signature: data.signature || '',
+      hmac: data.hmac || '',
+      timestamp: data.timestamp || '',
+      originalText: data.originalText || '',
+      originalUrl: data.url || ''
+    }
+    evidenceDialog.value = true
+  }
+}
+
+// 有效性验证
+const verifyDialog = ref(false)
+const verifyResult = ref({
+  signVerify: false,
+  hmacCheck: false,
+  integrityCheck: false,
+  valid: false
+})
+
+// 文本验证弹窗
+const textVerifyDialog = ref(false)
+const textVerifyForm = reactive({
+  text: '',
+  credIden: '',
+  evidenceIden: '',
+  hmac: ''
+})
+
+// 文件验证弹窗
+const fileVerifyDialog = ref(false)
+const fileVerifyForm = reactive({
+  credIden: '',
+  evidenceIden: '',
+  hmac: '',
+  file: null
+})
+const uploadRef = ref(null)
+const verifyLoading = ref(false)
+
+const handleVerify = async row => {
+  const res = await getEvidenceDataByIden({ evidenceIden: row.evidenceIden })
+  if (res?.code !== 100000) return
+  const data = res.data || {}
+
+  if (Number(row.type) === 1) {
+    textVerifyForm.credIden = row.credIden || ''
+    textVerifyForm.evidenceIden = row.evidenceIden
+    textVerifyForm.hmac = data.hmac || ''
+    textVerifyForm.text = data.originalText || ''
+    textVerifyDialog.value = true
+  } else {
+    fileVerifyForm.credIden = row.credIden || ''
+    fileVerifyForm.evidenceIden = row.evidenceIden
+    fileVerifyForm.hmac = data.hmac || ''
+    fileVerifyForm.file = null
+    fileVerifyDialog.value = true
+    await nextTick()
+    uploadRef.value?.clearFiles()
+  }
+}
+
+// 文件选择变化
+const handleFileChange = file => {
+  fileVerifyForm.file = file.raw
+}
+
+// 移除文件
+const handleFileRemove = () => {
+  fileVerifyForm.file = null
+}
+
+// 验证结果映射
+const applyVerifyResult = res => {
+  if (res?.code !== 100000) return false
+  const data = res.data || {}
+  const signVerify = data.signEqual === true
+  const hmacCheck = data.hmacEqual === true
+  const integrityCheck = data.hashEqual === true
+  verifyResult.value = {
+    signVerify,
+    hmacCheck,
+    integrityCheck,
+    valid: signVerify && hmacCheck && integrityCheck
+  }
+  return true
+}
+
+// 执行文本验证
+const doTextVerify = async() => {
+  if (!textVerifyForm.text) {
+    ElMessage.warning('请输入存证原文')
+    return
+  }
+  if (!textVerifyForm.hmac) {
+    ElMessage.warning('未获取到 HMAC 凭证,无法验证')
+    return
+  }
+  verifyLoading.value = true
+  try {
+    const res = await textEvidenceVerify({
+      evidenceIden: textVerifyForm.evidenceIden,
+      text: textVerifyForm.text,
+      hmac: textVerifyForm.hmac
+    })
+    if (applyVerifyResult(res)) {
+      textVerifyDialog.value = false
+      verifyDialog.value = true
+    }
+  } finally {
+    verifyLoading.value = false
+  }
+}
+
+// 执行文件验证
+const doFileVerify = async() => {
+  if (!fileVerifyForm.file) {
+    ElMessage.warning('请选择原始文件')
+    return
+  }
+  if (!fileVerifyForm.hmac) {
+    ElMessage.warning('未获取到 HMAC 凭证,无法验证')
+    return
+  }
+  verifyLoading.value = true
+  try {
+    const formData = new FormData()
+    formData.append('file', fileVerifyForm.file)
+    formData.append('evidenceIden', fileVerifyForm.evidenceIden)
+    formData.append('hmac', fileVerifyForm.hmac)
+
+    const res = await fileEvidenceVerify(formData)
+    if (applyVerifyResult(res)) {
+      fileVerifyDialog.value = false
+      verifyDialog.value = true
+    }
+  } finally {
+    verifyLoading.value = false
+  }
+}
+
+onMounted(() => {
+  fetchList()
+})
+</script>
+
+<style lang="scss" scoped>
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+  margin-top: 4px;
+}
+
+.verify-detail {
+  text-align: left;
+  margin-top: 10px;
+
+  .verify-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+
+    span {
+      width: 110px;
+      font-weight: 500;
+    }
+  }
+}
+</style>

+ 740 - 0
src/view/sysApplication/pki/certManager/index.vue

@@ -0,0 +1,740 @@
+<template>
+  <div>
+    <div class="gva-search-box">
+      <el-form
+        ref="elSearchFormRef"
+        :inline="true"
+        :model="searchInfo"
+        class="demo-form-inline"
+        @keyup.enter="onSubmit"
+      >
+        <el-form-item label="证书名称">
+          <el-input
+            v-model="searchInfo.certName"
+            placeholder="请输入证书名称"
+          />
+        </el-form-item>
+        <el-form-item label="证书状态">
+          <el-select
+            v-model="searchInfo.status"
+            clearable
+            placeholder="请选择证书状态"
+          >
+            <el-option
+              v-for="item in certStatusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+
+            icon="search"
+            type="primary"
+            @click="onSubmit"
+          >查询
+          </el-button>
+          <el-button
+
+            icon="refresh"
+            @click="onReset"
+          >重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="gva-table-box">
+      <div class="gva-btn-list">
+        <el-button
+
+          icon="plus"
+          type="primary"
+          @click="addCertDialog()"
+        >签发证书
+        </el-button>
+      </div>
+      <!-- 表格 -->
+      <el-table
+        :data="tableData"
+        style="width: 100%"
+      >
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="证书序列号"
+          prop="certSerialNumber"
+          width="300"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="证书名称"
+          prop="certName"
+          width="250"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="证书算法"
+          prop="certAlgo"
+          width="80"
+        >
+          <template #default="scope">
+            {{ scope.row.certAlgo === '1' ? 'RSA' : scope.row.certAlgo === '2' ? 'SM2' : scope.row.certAlgo }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="父证书序列号"
+          prop="parentSerial"
+          width="300"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="证书有效期"
+          prop="certValidTime"
+          width="340"
+        >
+          <template #default="scope">
+            {{ formatCertificateTime(scope.row.validFrom) }} ~ {{ formatCertificateTime(scope.row.validTo) }}
+          </template>
+        </el-table-column>
+<!--        <el-table-column
+          label="创建时间"
+          prop="createdTime"
+          width="170"
+        >
+          <template #default="scope">
+            {{ formatCertificateTime(scope.row.createdTime) }}
+          </template>
+        </el-table-column>-->
+        <el-table-column
+          label="证书状态"
+          prop="status"
+          width="120"
+        >
+          <template #default="scope">
+            {{ certStatusOptions.find(item => item.value === scope.row.status)?.label || scope.row.status }}
+          </template>
+        </el-table-column>
+        <el-table-column
+
+          header-align="center"
+          fixed="right"
+          label="操作"
+        >
+          <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'"
+              link
+              type="primary"
+              @click="handleFreeze(scope.row)"
+            >冻结</el-button>
+            <el-button
+              v-else-if="scope.row.status === '3'"
+              link
+              type="primary"
+              @click="handleUnfreeze(scope.row)"
+            >解冻</el-button>
+
+            <!-- 续期:有效 / 冻结 / 已过期 可续 -->
+            <el-button
+              v-if="scope.row.status === '1' || scope.row.status === '2' "
+              link
+              type="primary"
+              @click="handleRenew(scope.row)"
+            >续期</el-button>
+
+            <!-- 吊销:未吊销的都可吊销 -->
+            <el-button
+              v-if="scope.row.status !== '4'"
+
+              link
+              type="danger"
+              @click="handleRevoke(scope.row)"
+            >吊销</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-dialog
+        v-model="addCertDialogVisible"
+        title="签发证书"
+        width="50%"
+      >
+        <el-form
+          ref="addCertFormRef"
+          :model="addCertForm"
+          :rules="addCertRules"
+          label-width="120px"
+        >
+          <el-form-item
+            label="证书算法"
+            prop="keyAlgorithm"
+          >
+            <el-select
+              v-model="addCertForm.keyAlgorithm"
+              placeholder="请选择证书算法"
+              @change="handleKeyAlgorithmChange"
+            >
+              <el-option
+                v-for="item in keyAlgoOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            v-if="addCertForm.keyAlgorithm === '1'"
+            label="密钥长度"
+            prop="keySize"
+          >
+            <el-select
+              v-model="addCertForm.keySize"
+              placeholder="请选择密钥长度"
+            >
+              <el-option
+                v-for="item in [{ label: '2048', value: '2048' }, { label: '4096', value: '4096' }]"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            label="选择CA"
+            prop="rootCa"
+          >
+            <el-select
+              v-model="addCertForm.rootCa"
+              placeholder="请选择CA"
+              :disabled="!addCertForm.keyAlgorithm"
+            >
+              <el-option
+                v-for="item in rootCaList"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item
+            label="证书名称"
+            prop="certName"
+          >
+            <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-form-item>
+          <el-form-item
+            label="国家"
+            prop="country"
+          >
+            <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-form-item>
+          <el-form-item
+            label="组织单位"
+            prop="organizationalUnit"
+          >
+            <el-input
+              v-model="addCertForm.organizationalUnit"
+              placeholder="请输入证书OU项"
+            />
+          </el-form-item>
+          <el-form-item
+            label="省份"
+            prop="state"
+          >
+            <el-input
+              v-model="addCertForm.state"
+              placeholder="请输入证书S项"
+            />
+          </el-form-item>
+          <el-form-item
+            label="城市"
+            prop="locality"
+          >
+            <el-input
+              v-model="addCertForm.locality"
+              placeholder="请输入证书L项"
+            />
+          </el-form-item>
+          <el-form-item
+            label="证书有效期"
+            prop="validityDays"
+          >
+            <el-input
+              v-model="addCertForm.validityDays"
+              type="number"
+              placeholder="请输入证书有效期(天)"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button
+              v-preReClick
+              type="primary"
+              @click="submitAddCertForm"
+            >提交</el-button>
+            <el-button @click="addCertDialogVisible = false">取消</el-button>
+          </el-form-item>
+        </el-form>
+      </el-dialog>
+    </div>
+    <!-- 分页栏 -->
+    <div class="gva-pagination">
+      <el-pagination
+        :current-page="page"
+        :page-size="pageSize"
+        :page-sizes="[10, 30, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      />
+    </div>
+
+  </div>
+</template>
+<script setup>
+import { ref } from 'vue'
+import {
+  addCertificate,
+  downloadPemCombined,
+  downloadPkcs12,
+  freezeCertificate,
+  listCertificates, renewCertificate, revokeCertificate, unfreezeCertificate,
+} from '@/api/sysApplication/certificate'
+import { ElMessage, ElMessageBox } from 'element-plus'
+const searchInfo = ref({})
+const addCertFormRef = ref(null)
+const elSearchFormRef= ref(null)
+// 搜索
+const onSubmit = () => {
+  elSearchFormRef.value?.validate(async valid => {
+    if (!valid) return
+    page.value = 1
+    pageSize.value = 10
+    await getTableData()
+  })
+}
+// 重置
+const onReset = () => {
+  searchInfo.value = {}
+  getTableData()
+}
+const page = ref(1)
+const total = ref(0)
+const pageSize = ref(10)
+const tableData = ref([])
+// 翻页
+const handleSizeChange = val => {
+  pageSize.value = val
+  getTableData()
+}
+// 每页条数
+const handleCurrentChange = val => {
+  page.value = val
+  getTableData()
+}
+const getTableData = async() => {
+  const res = await listCertificates({
+    pageNum: page.value,
+    pageSize: pageSize.value,
+    certUsage: 'PERSONAL',
+    ...searchInfo.value
+  })
+  tableData.value = res.data.list
+  total.value = res.data.total
+}
+getTableData()
+const rootCaList = ref([])
+let rootCaRequestId = 0
+const getRootCaList = async(certAlgo) => {
+  const requestId = ++rootCaRequestId
+  rootCaList.value = []
+  if (!certAlgo) return
+
+  try {
+    const res = await listCertificates({
+      pageNum: 1,
+      pageSize: 99999,
+      certUsage: 'ROOT_CA',
+      certAlgo
+    })
+    if (requestId !== rootCaRequestId) return
+    rootCaList.value = res.data.list.map(item => ({
+      label: item.certName,
+      value: item.id
+    }))
+  } catch (err) {
+    if (requestId === rootCaRequestId) {
+      ElMessage.error(err.message || '获取CA列表失败')
+    }
+  }
+}
+
+const certStatusOptions = [
+  { label: '有效', value: '1' },
+  { label: '过期', value: '2' },
+  { label: '冻结', value: '3' },
+  { label: '吊销', value: '4' }
+]
+const formatCertificateTime = value => value ? String(value).replace('T', ' ') : ''
+const addCertForm = ref({
+  rootCa: '',
+  certName: '',
+  commonName: '',
+  country: 'CN',
+  organization: '',
+  organizationalUnit: '',
+  state: '',
+  locality: '',
+  keyAlgorithm: '',
+  keySize: '2048',
+  validityDays: '',
+  isRoot: '1',
+  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' },
+    {
+      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: 'blur' }],
+  keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'blur' }],
+  keySize: [{ required: true, message: '请选择密钥长度', trigger: 'blur' }],
+  validityDays: [
+    { required: true, message: '请输入证书有效期', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        if (value === '' || value === null || value === undefined) {
+          callback()
+          return
+        }
+        const days = Number(value)
+        if (days <= 1 || days >= 7300 || !Number.isFinite(days)) {
+          callback(new Error('证书有效期必须大于1且小于7300天'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
+}
+
+const addCertDialogVisible = ref(false)
+const keyAlgoOptions = [
+  { label: 'RSA', value: '1' },
+  { label: 'SM2', value: '2' }
+]
+
+const handleKeyAlgorithmChange = async(keyAlgorithm) => {
+  addCertForm.value.rootCa = ''
+  addCertFormRef.value?.clearValidate('rootCa')
+  await getRootCaList(keyAlgorithm)
+}
+
+const addCertDialog = () => {
+
+  addCertForm.value = {
+    issuerId: '',
+    rootCa: '',
+    certName: '',
+    commonName: '',
+    country: 'CN',
+    organization: '',
+    organizationalUnit: '',
+    state: '',
+    locality: '',
+    keyAlgorithm: '',
+    keySize: '2048',
+    isRoot: '1',
+    isCa: '1',
+    certUsage: '',
+    validityDays: '365',
+  }
+  addCertDialogVisible.value = true
+  addCertFormRef.value?.clearValidate()
+  console.log('addCertDialogVisible', addCertDialogVisible.value)
+}
+const submitAddCertForm = () => {
+  addCertFormRef.value?.validate(async valid => {
+    if (!valid) return
+    const res = await addCertificate({
+      ...addCertForm.value,
+      keySize: addCertForm.value.keyAlgorithm === '2' ? '256' : addCertForm.value.keySize,
+      isRoot: 0,
+      isCa: 0,
+      certUsage: 'PERSONAL',
+      issuerId: addCertForm.value.rootCa
+    })
+    if (res.code === 100000) {
+      ElMessage.success('申请证书成功')
+      // 关闭对话框
+      addCertDialogVisible.value = false
+      // 刷新表格数据
+      await getTableData()
+    }
+  })
+}
+
+/**
+ * 触发下载
+ * @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, `${row.certSerialNumber}.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, `${row.certSerialNumber}.p12`)
+    }
+  } catch (err) {
+    // 用户取消弹框
+    if (err === 'cancel' || err === 'close') return
+    console.error('下载失败', err)
+    ElMessage.error('下载失败')
+  }
+}
+
+/**
+ * 通用 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)
+}
+// 冻结
+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 的天数'
+          if (num > 3650) return '续期天数不能超过 3650 天'
+          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)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.download-dropdown {
+  vertical-align: middle;
+  line-height: 1;
+  margin-right: 12px;
+}
+</style>

+ 839 - 0
src/view/sysApplication/pki/rootCaManager/index.vue

@@ -0,0 +1,839 @@
+<template>
+  <div>
+    <div class="gva-search-box">
+      <el-form
+        ref="elSearchFormRef"
+        :inline="true"
+        :model="searchInfo"
+        class="demo-form-inline"
+        @keyup.enter="onSubmit"
+      >
+        <el-form-item label="证书名称">
+          <el-input
+            v-model="searchInfo.certName"
+            placeholder="请输入证书名称"
+          />
+        </el-form-item>
+        <el-form-item label="证书状态">
+          <el-select
+            v-model="searchInfo.status"
+            clearable
+            placeholder="请选择证书状态"
+          >
+            <el-option
+              v-for="item in certStatusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            icon="search"
+            type="primary"
+            @click="onSubmit"
+          >查询
+          </el-button>
+          <el-button
+            icon="refresh"
+            @click="onReset"
+          >重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="gva-table-box">
+
+      <div class="gva-btn-list">
+        <el-button
+          icon="plus"
+          type="primary"
+          @click="issueCertDialog()"
+        >签发根证书
+        </el-button>
+        <el-button
+          icon="plus"
+          type="primary"
+          @click="addCertDialog()"
+        >上传根证书
+        </el-button>
+      </div>
+      <!-- 表格 -->
+      <el-table
+        :data="tableData"
+        style="width: 100%"
+      >
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="证书序列号"
+          prop="certSerialNumber"
+          width="350"
+        />
+        <el-table-column
+          align="center"
+          header-align="center"
+          label="证书名称"
+          prop="certName"
+          width="280"
+        />
+        <el-table-column
+          label="证书算法"
+          prop="certAlgo"
+          width="100"
+        >
+          <template #default="scope">
+            {{ scope.row.certAlgo === '1' ? 'RSA' : scope.row.certAlgo === '2' ? 'SM2' : scope.row.certAlgo }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="证书有效期"
+          prop="certValidTime"
+          width="340"
+        >
+          <template #default="scope">
+            {{ formatCertificateTime(scope.row.validFrom) }} ~ {{ formatCertificateTime(scope.row.validTo) }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="创建时间"
+          prop="createdTime"
+          width="170"
+        >
+          <template #default="scope">
+            {{ formatCertificateTime(scope.row.createdTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="证书状态"
+          prop="status"
+          width="120"
+        >
+          <template #default="scope">
+            {{ certStatusOptions.find(item => item.value === scope.row.status)?.label || scope.row.status }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          fixed="right"
+          label="操作"
+          width="260"
+        >
+          <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="handleFreeze(scope.row)"
+            >冻结</el-button>
+            <el-button
+              v-else-if="scope.row.status === '3'"
+              v-preReClick
+              link
+              type="primary"
+              @click="handleUnfreeze(scope.row)"
+            >解冻</el-button>
+
+            &lt;!&ndash; 续期:有效 / 冻结 / 已过期 可续 &ndash;&gt;
+            <el-button
+              v-preReClick
+              link
+              type="primary"
+              @click="handleRenew(scope.row)"
+            >续期</el-button>
+
+            &lt;!&ndash; 吊销:未吊销的都可吊销 &ndash;&gt;
+            <el-button
+              v-if="scope.row.status !== '4'"
+              v-preReClick
+              link
+              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>
+
+      <el-dialog
+        v-model="addCertDialogVisible"
+        title="上传根证书"
+        width="50%"
+        @close="handleClose"
+      >
+        <el-form
+          ref="uploadFormRef"
+          :model="uploadForm"
+          :rules="uploadRules"
+          label-width="100px"
+        >
+          <!-- 证书名称(可选) -->
+          <el-form-item
+            label="证书名称"
+            prop="certName"
+          >
+            <el-input
+              v-model="uploadForm.certName"
+              placeholder="请输入证书名称(可选)"
+              clearable
+            />
+          </el-form-item>
+
+          <!-- 文件上传 -->
+          <el-form-item label="证书文件">
+            <el-upload
+              ref="uploadRef"
+              drag
+              :auto-upload="false"
+              :limit="1"
+              accept=".pem"
+              :on-change="handleFileChange"
+              :on-exceed="handleExceed"
+              :file-list="fileList"
+            >
+              <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+              <div class="el-upload__text">
+                拖拽文件到此处,或 <em>点击上传</em>
+              </div>
+              <template #tip>
+                <div class="el-upload__tip">
+                  请上传合并的 PEM 文件(证书 [+ 证书链] + 私钥)
+                </div>
+              </template>
+            </el-upload>
+          </el-form-item>
+        </el-form>
+
+        <template #footer>
+          <el-button @click="handleClose">取消</el-button>
+          <el-button
+            v-preReClick
+            type="primary"
+            :loading="uploading"
+            @click="handleSubmit"
+          >
+            确定上传
+          </el-button>
+        </template>
+      </el-dialog>
+
+      <el-dialog
+        v-model="issueCertDialogVisible"
+        title="签发根证书"
+        width="50%"
+      >
+        <el-form
+          ref="addCertFormRef"
+          :model="addCertForm"
+          :rules="addCertRules"
+          label-width="120px"
+        >
+
+          <el-form-item
+            label="证书算法"
+            prop="keyAlgorithm"
+          >
+            <el-select
+              v-model="addCertForm.keyAlgorithm"
+              placeholder="请选择证书算法"
+            >
+              <el-option
+                v-for="item in keyAlgoOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            v-if="addCertForm.keyAlgorithm === '1'"
+            label="密钥长度"
+            prop="keySize"
+          >
+            <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-form-item>
+          <el-form-item
+            label="公用名"
+            prop="commonName"
+          >
+            <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-form-item>
+          <el-form-item
+            label="组织"
+            prop="organization"
+          >
+            <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-form-item>
+          <el-form-item
+            label="省份"
+            prop="state"
+          >
+            <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-form-item>
+          <el-form-item
+            label="证书有效期"
+            prop="validityDays"
+          >
+            <el-input
+              v-model="addCertForm.validityDays"
+              type="number"
+              placeholder="请输入证书有效期(天)"
+            />
+          </el-form-item>
+        </el-form>
+
+        <template #footer>
+          <el-button @click="issueCertDialogVisible = false">取消</el-button>
+          <el-button
+            v-preReClick
+            type="primary"
+            @click="submitAddCertForm"
+          >提交</el-button>
+        </template>
+      </el-dialog>
+
+    </div>
+    <!-- 分页栏 -->
+    <div class="gva-pagination">
+      <el-pagination
+        :current-page="page"
+        :page-size="pageSize"
+        :page-sizes="[10, 30, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      />
+    </div>
+
+  </div>
+</template>
+<script setup>
+import { ref } from 'vue'
+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)
+// 搜索
+const onSubmit = () => {
+  elSearchFormRef.value?.validate(async valid => {
+    if (!valid) return
+    page.value = 1
+    pageSize.value = 10
+    await getTableData()
+  })
+}
+// 重置
+const onReset = () => {
+  searchInfo.value = {}
+  getTableData()
+}
+const page = ref(1)
+const total = ref(0)
+const pageSize = ref(10)
+const tableData = ref([])
+// 翻页
+const handleSizeChange = val => {
+  pageSize.value = val
+  getTableData()
+}
+// 每页条数
+const handleCurrentChange = val => {
+  page.value = val
+  getTableData()
+}
+const getTableData = async() => {
+  const res = await listCertificates({
+    pageNum: page.value,
+    pageSize: pageSize.value,
+    certUsage: 'ROOT_CA',
+    ...searchInfo.value
+  })
+  tableData.value = res.data.list
+  total.value = res.data.total
+}
+getTableData()
+
+const certStatusOptions = [
+  { label: '有效', value: '1' },
+  { label: '过期', value: '2' },
+  { label: '冻结', value: '3' },
+  { label: '吊销', value: '4' }
+]
+const formatCertificateTime = value => value ? String(value).replace('T', ' ') : ''
+const addCertForm = ref({
+  certName: '',
+  commonName: '',
+  country: 'CN',
+  organization: '',
+  organizationUnit: '',
+  state: '',
+  locality: '',
+  phone: '',
+  keyAlgorithm: '1',
+  keySize: '2048',
+  validityDays: '',
+  isRoot: '1',
+  isCa: '1',
+  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' },
+    {
+      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: 'blur' }],
+  keySize: [{ required: true, message: '请选择密钥长度', trigger: 'blur' }],
+  validityDays: [
+    { required: true, message: '请输入证书有效期', trigger: 'blur' },
+    {
+      validator: (_rule, value, callback) => {
+        if (value === '' || value === null || value === undefined) {
+          callback()
+          return
+        }
+        const days = Number(value)
+        if (days <= 1 || days > 7300 || !Number.isFinite(days)) {
+          callback(new Error('证书有效期必须大于1且小于7300天'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
+}
+
+const addCertDialogVisible = ref(false)
+const issueCertDialogVisible = ref(false)
+const keyAlgoOptions = [
+  { label: 'RSA', value: '1' },
+  { label: 'SM2', value: '2' }
+]
+const uploadForm = ref({
+
+})
+const addCertDialog = () => {
+  addCertDialogVisible.value = true
+  console.log('addCertDialogVisible', addCertDialogVisible.value)
+}
+const issueCertDialog = () => {
+  addCertForm.value = {
+    certName: '',
+    commonName: '',
+    country: 'CN',
+    organization: '',
+    organizationUnit: '',
+    state: '',
+    locality: '',
+    phone: '',
+    keyAlgorithm: '1',
+    keySize: '2048',
+    validityDays: '365',
+    isRoot: '1',
+    isCa: '1',
+    certUsage: 'ROOT_CA',
+    remark: ''
+  }
+  addCertFormRef.value?.clearValidate()
+  issueCertDialogVisible.value = true
+}
+
+const submitAddCertForm = () => {
+  addCertFormRef.value?.validate(async valid => {
+    if (!valid) return
+    const res = await addRootCa({
+      ...addCertForm.value,
+      keySize: addCertForm.value.keyAlgorithm === '2' ? '256' : addCertForm.value.keySize,
+      isRoot: 1,
+      isCa: 1,
+      certUsage: 'BOTH'
+    })
+    if (res.code === 100000) {
+      ElMessage.success('申请根证书成功')
+      issueCertDialogVisible.value = false
+      await getTableData()
+    }
+  })
+}
+// 文件选择变化
+const handleFileChange = (file) => {
+  uploadForm.value.file = file.raw
+  fileList.value = [file]
+}
+
+// 超出限制
+const handleExceed = () => {
+  ElMessage.warning('只能上传一个文件,请先移除已选文件')
+}
+
+// 提交
+const handleSubmit = async() => {
+  uploadFormRef.value?.validate(async valid => {
+    if (!valid) return
+
+    if (!uploadForm.value.file) {
+      ElMessage.warning('请先选择证书文件')
+      return
+    }
+
+    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 = await 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)
+const uploading = ref(false)
+// 关闭弹窗,重置状态
+const handleClose = () => {
+  addCertDialogVisible.value = false
+  uploadForm.value.certName = ''
+  uploadForm.value.file = null
+  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, `${row.certSerialNumber}.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, `${row.certSerialNumber}.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: '取消'
+    })
+    const res = await deleteCertificate(row.id)
+    if (res.code === 100000) {
+      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>

+ 405 - 0
src/view/sysApplication/sysApplication/index.vue

@@ -0,0 +1,405 @@
+<template>
+  <div>
+    <div class="gva-search-box">
+      <el-form
+        ref="elSearchFormRef"
+        :inline="true"
+        :model="searchInfo"
+        class="demo-form-inline"
+        @keyup.enter="onSubmit"
+      >
+        <el-form-item
+          label="应用名称"
+          prop="name"
+        >
+          <el-input
+            v-model="searchInfo.name"
+            placeholder="请输入应用名称"
+            clearable
+          />
+        </el-form-item>
+        <el-form-item
+          label="启用状态"
+          prop="status"
+        >
+          <el-select
+            v-model="searchInfo.status"
+            placeholder="请选择启用状态"
+            clearable
+          >
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            icon="search"
+            @click="onSubmit"
+          >查询</el-button>
+          <el-button
+            icon="refresh"
+            @click="onReset"
+          >重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="gva-table-box">
+      <div class="gva-btn-list">
+        <el-button
+          type="primary"
+          icon="plus"
+          @click="openDialog"
+        >新增应用</el-button>
+      </div>
+      <el-table
+        :data="tableData"
+        style="width: 100%"
+        row-key="id"
+      >
+        <el-table-column
+          align="left"
+          label="应用名称"
+          prop="name"
+          width="180"
+        />
+        <el-table-column
+          align="center"
+          label="key"
+          prop="apiKey"
+          width="320"
+        />
+        <el-table-column
+          align="center"
+          label="安全密钥"
+          prop="apiSecret"
+          width="350"
+        >
+          <template #default="{ row }">
+            <span>
+              {{ row.apiSecret }}
+              <el-button
+                link
+                size="small"
+                type="primary"
+                style="margin-left: 10px;"
+                @click="copySecret(row)"
+              >
+                复制
+              </el-button>
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="启用状态"
+          prop="status"
+          width="120"
+        >
+          <template #default="{ row }">
+            <el-switch
+              v-model="row.status"
+              inline-prompt
+              :active-value="1"
+              :inactive-value="2"
+              @change="v => switchEnable(row, v)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="创建日期"
+          width="220"
+        >
+          <template #default="{ row }">
+            {{ formatDate(row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          align="center"
+          label="操作"
+          fixed="right"
+        >
+          <template #default="{ row }">
+            <el-button
+              type="primary"
+              link
+              icon="edit"
+              class="table-button"
+              @click="resetSecret(row)"
+            >重置密钥</el-button>
+            <el-button
+              type="primary"
+              link
+              icon="delete"
+              @click="deleteRow(row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="gva-pagination">
+        <el-pagination
+          layout="total, sizes, prev, pager, next, jumper"
+          :current-page="page"
+          :page-size="pageSize"
+          :page-sizes="[10, 30, 50, 100]"
+          :total="total"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </div>
+
+    <el-dialog
+      v-model="dialogFormVisible"
+      :before-close="closeDialog"
+      title="添加应用"
+      destroy-on-close
+    >
+      <el-form
+        ref="elFormRef"
+        :model="formData"
+        label-position="right"
+        :rules="rule"
+        label-width="120px"
+      >
+        <el-form-item
+          label="应用名称"
+          prop="name"
+        >
+          <el-input
+            v-model="formData.name"
+            clearable
+            placeholder="请输入应用名称"
+          />
+        </el-form-item>
+        <el-form-item
+          label="启用状态"
+          prop="status"
+        >
+          <el-radio-group v-model="formData.status">
+            <el-radio
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.value"
+            >
+              {{ item.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="closeDialog">取 消</el-button>
+          <el-button
+            type="primary"
+            @click="enterDialog"
+          >确 定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  getApplicationCredentialList,
+  createApplicationCredential,
+  deleteApplicationCredential,
+  resetApplicationSecret,
+  getApplicationSecret,
+  changeApplicationStatus
+} from '@/api/sysApplication/applicationCredentials'
+import { formatDate } from '@/utils/format'
+
+defineOptions({
+  name: 'SysApplication'
+})
+
+const statusOptions = [
+  { value: 1, label: '启用' },
+  { value: 2, label: '禁用' }
+]
+
+// =========== 表格控制部分 ===========
+const page = ref(1)
+const total = ref(0)
+const pageSize = ref(10)
+const tableData = ref([])
+const searchInfo = ref({})
+const elSearchFormRef = ref()
+
+const getTableData = async() => {
+  const res = await getApplicationCredentialList({
+    pageNum: page.value,
+    pageSize: pageSize.value,
+    name: searchInfo.value.name || undefined,
+    status: searchInfo.value.status || undefined
+  })
+  if (res.code === 100000) {
+    tableData.value = res.data?.list || []
+    total.value = res.data?.total || 0
+  }
+}
+
+getTableData()
+
+// 搜索
+const onSubmit = () => {
+  page.value = 1
+  getTableData()
+}
+
+// 重置
+const onReset = () => {
+  searchInfo.value = {}
+  page.value = 1
+  getTableData()
+}
+
+// 每页条数
+const handleSizeChange = val => {
+  pageSize.value = val
+  getTableData()
+}
+
+// 翻页
+const handleCurrentChange = val => {
+  page.value = val
+  getTableData()
+}
+// ============== 表格控制部分结束 ===============
+
+// 启用/禁用
+const switchEnable = async(row, v) => {
+  // el-switch 变更后 v-model 已更新,失败时回滚
+  const prevStatus = v === 1 ? 2 : 1
+  const res = await changeApplicationStatus(row.id, v)
+  if (res.code === 100000) {
+    ElMessage.success('修改成功')
+  } else {
+    row.status = prevStatus
+  }
+}
+
+// 复制安全密钥(列表中的 apiSecret 已脱敏,这里查询明文)
+const copyToClipboard = async text => {
+  try {
+    if (navigator.clipboard) {
+      await navigator.clipboard.writeText(text)
+    } else {
+      const textArea = document.createElement('textarea')
+      textArea.value = text
+      document.body.appendChild(textArea)
+      textArea.select()
+      document.execCommand('copy')
+      document.body.removeChild(textArea)
+    }
+    ElMessage.success('复制安全密钥成功,可以粘贴')
+  } catch (err) {
+    console.error('复制失败', err)
+    ElMessage.error('复制失败')
+  }
+}
+
+const copySecret = async row => {
+  const res = await getApplicationSecret(row.id)
+  if (res.code === 100000) {
+    await copyToClipboard(res.data || '')
+  }
+}
+
+// 重置密钥
+const resetSecret = row => {
+  ElMessageBox.confirm('确定要重置该应用的密钥吗?重置后旧密钥将立即失效。', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async() => {
+    const res = await resetApplicationSecret(row.id)
+    if (res.code === 100000) {
+      ElMessage.success('重置成功')
+      await getTableData()
+    }
+  }).catch(() => {})
+}
+
+// 删除
+const deleteRow = row => {
+  ElMessageBox.confirm('确定要删除该应用吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async() => {
+    const res = await deleteApplicationCredential(row.id)
+    if (res.code === 100000) {
+      ElMessage.success('删除成功')
+      if (tableData.value.length === 1 && page.value > 1) {
+        page.value--
+      }
+      await getTableData()
+    }
+  }).catch(() => {})
+}
+
+// =========== 新增应用弹窗 ===========
+const dialogFormVisible = ref(false)
+const elFormRef = ref()
+const formData = ref({
+  name: '',
+  status: 1
+})
+
+const rule = {
+  name: [
+    { required: true, message: '请输入应用名称', trigger: 'blur' },
+    { max: 255, message: '长度不能超过255个字符', trigger: 'blur' }
+  ]
+}
+
+// 打开弹窗
+const openDialog = () => {
+  formData.value = {
+    name: '',
+    status: 1
+  }
+  dialogFormVisible.value = true
+  elFormRef.value?.clearValidate()
+}
+
+// 关闭弹窗
+const closeDialog = () => {
+  dialogFormVisible.value = false
+  formData.value = {
+    name: '',
+    status: 1
+  }
+}
+
+// 弹窗确定
+const enterDialog = () => {
+  elFormRef.value?.validate(async valid => {
+    if (!valid) return
+    const res = await createApplicationCredential({ name: formData.value.name })
+    if (res.code === 100000) {
+      // 后端创建时默认启用,若选择禁用则再调用一次状态接口
+      if (formData.value.status === 2 && res.data?.id) {
+        await changeApplicationStatus(res.data.id, 2)
+      }
+      ElMessage.success('创建成功')
+      dialogFormVisible.value = false
+      await getTableData()
+    }
+  })
+}
+</script>
+
+<style>
+</style>