|
|
@@ -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>
|
|
|
+
|
|
|
+ <!– 续期:有效 / 冻结 / 已过期 可续 –>
|
|
|
+ <el-button
|
|
|
+ v-preReClick
|
|
|
+ link
|
|
|
+ type="primary"
|
|
|
+ @click="handleRenew(scope.row)"
|
|
|
+ >续期</el-button>
|
|
|
+
|
|
|
+ <!– 吊销:未吊销的都可吊销 –>
|
|
|
+ <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>
|