Selaa lähdekoodia

1.证书管理表格显示父证书序列号,调整宽度
2.调整日期字符串的格式。
3.调整表单校验的触发规则和关闭弹窗重置表单校验
4.限制文件上传大小
5.印章移除延期功能
6.产出文件不再自动下载,提供下载按钮和下载链接
7.调整页面宽度不固定的问题
8.新增文件转换风险检测

wangkun 3 viikkoa sitten
vanhempi
commit
2e298c1264

+ 15 - 0
src/api/sysApplication/signature.js

@@ -18,6 +18,21 @@ export const ofdToPdf = data => {
     responseType: 'blob'
   })
 }
+export const checkPdfFontRisk = data => {
+  return service({
+    url: '/v1/signature/check-pdf-font-risk',
+    method: 'post',
+    data,
+  })
+}
+
+export const checkOfdToPdf = data => {
+  return service({
+    url: '/v1/signature/check-ofd-to-pdf',
+    method: 'post',
+    data,
+  })
+}
 
 // OFD电子签章
 export const signOFD = ({ sealId, x, y, signType, file }) => {

+ 71 - 36
src/view/sysApplication/pki/certManager/index.vue

@@ -66,38 +66,53 @@
           header-align="center"
           label="证书序列号"
           prop="certSerialNumber"
-          width="350"
+          width="300"
         />
         <el-table-column
           align="center"
           header-align="center"
           label="证书名称"
           prop="certName"
-          width="280"
+          width="250"
         />
         <el-table-column
+          align="center"
+          header-align="center"
           label="证书算法"
           prop="certAlgo"
-          width="100"
+          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">
-            {{ scope.row.validFrom }} ~ {{ scope.row.validTo }}
+            {{ formatCertificateTime(scope.row.validFrom) }} ~ {{ formatCertificateTime(scope.row.validTo) }}
           </template>
         </el-table-column>
-        <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"
@@ -108,9 +123,10 @@
           </template>
         </el-table-column>
         <el-table-column
+
+          header-align="center"
           fixed="right"
           label="操作"
-          width="260"
         >
           <template #default="scope">
             <!-- 下载(保留之前的下拉) -->
@@ -135,14 +151,12 @@
             <!-- 冻结 / 解冻:同一按钮,按状态切换 -->
             <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)"
@@ -150,7 +164,7 @@
 
             <!-- 续期:有效 / 冻结 / 已过期 可续 -->
             <el-button
-
+              v-if="scope.row.status === '1' || scope.row.status === '2' "
               link
               type="primary"
               @click="handleRenew(scope.row)"
@@ -186,6 +200,7 @@
             <el-select
               v-model="addCertForm.keyAlgorithm"
               placeholder="请选择证书算法"
+              @change="handleKeyAlgorithmChange"
             >
               <el-option
                 v-for="item in keyAlgoOptions"
@@ -219,6 +234,7 @@
             <el-select
               v-model="addCertForm.rootCa"
               placeholder="请选择CA"
+              :disabled="!addCertForm.keyAlgorithm"
             >
               <el-option
                 v-for="item in rootCaList"
@@ -380,19 +396,31 @@ const getTableData = async() => {
   total.value = res.data.total
 }
 getTableData()
-const rootCaList = ref()
-const getRootCaList = async() => {
-  const res = await listCertificates({
-    pageNum: page.value,
-    pageSize: pageSize.value,
-    certUsage: 'ROOT_CA'
-  })
-  rootCaList.value = res.data.list.map(item => ({
-    label: item.certName,
-    value: item.id
-  }))
+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列表失败')
+    }
+  }
 }
-getRootCaList()
 
 const certStatusOptions = [
   { label: '有效', value: '1' },
@@ -400,6 +428,7 @@ const certStatusOptions = [
   { label: '冻结', value: '3' },
   { label: '吊销', value: '4' }
 ]
+const formatCertificateTime = value => value ? String(value).replace('T', ' ') : ''
 const addCertForm = ref({
   rootCa: '',
   certName: '',
@@ -483,9 +512,9 @@ const addCertRules = {
   organizationalUnit: [optionalDnFieldRule('OU')],
   state: [optionalDnFieldRule('ST')],
   locality: [optionalDnFieldRule('L')],
-  rootCa: [{ required: true, message: '请选择CA', trigger: 'change' }],
-  keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'change' }],
-  keySize: [{ required: true, message: '请选择密钥长度', trigger: 'change' }],
+  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' },
     {
@@ -495,8 +524,8 @@ const addCertRules = {
           return
         }
         const days = Number(value)
-        if (days <= 1 || days >= 3650 || !Number.isFinite(days)) {
-          callback(new Error('证书有效期必须大于1且小于3650天'))
+        if (days <= 1 || days >= 7300 || !Number.isFinite(days)) {
+          callback(new Error('证书有效期必须大于1且小于7300天'))
         } else {
           callback()
         }
@@ -512,7 +541,14 @@ const keyAlgoOptions = [
   { label: 'SM2', value: '2' }
 ]
 
+const handleKeyAlgorithmChange = async(keyAlgorithm) => {
+  addCertForm.value.rootCa = ''
+  addCertFormRef.value?.clearValidate('rootCa')
+  await getRootCaList(keyAlgorithm)
+}
+
 const addCertDialog = () => {
+
   addCertForm.value = {
     issuerId: '',
     rootCa: '',
@@ -523,7 +559,7 @@ const addCertDialog = () => {
     organizationalUnit: '',
     state: '',
     locality: '',
-    keyAlgorithm: '1',
+    keyAlgorithm: '',
     keySize: '2048',
     isRoot: '1',
     isCa: '1',
@@ -531,6 +567,7 @@ const addCertDialog = () => {
     validityDays: '365',
   }
   addCertDialogVisible.value = true
+  addCertFormRef.value?.clearValidate()
   console.log('addCertDialogVisible', addCertDialogVisible.value)
 }
 const submitAddCertForm = () => {
@@ -546,13 +583,11 @@ const submitAddCertForm = () => {
     })
     if (res.code === 100000) {
       ElMessage.success('申请证书成功')
-    } else {
-      ElMessage.error(res.msg)
+      // 关闭对话框
+      addCertDialogVisible.value = false
+      // 刷新表格数据
+      await getTableData()
     }
-    // 关闭对话框
-    addCertDialogVisible.value = false
-    // 刷新表格数据
-    await getTableData()
   })
 }
 
@@ -566,7 +601,7 @@ const handleDownload = async(row, format) => {
     let res
     if (format === 'pem') {
       res = await downloadPemCombined({ id: row.id })
-      saveBlob(res, `certificate-${row.id}.pem`)
+      saveBlob(res, `${row.certSerialNumber}.pem`)
     } else if (format === 'p12') {
       // p12 需要密码,弹框让用户输入
       const { value: password } = await ElMessageBox.prompt(
@@ -583,7 +618,7 @@ const handleDownload = async(row, format) => {
         }
       )
       res = await downloadPkcs12({ id: row.id, password })
-      saveBlob(res, `certificate-${row.id}.p12`)
+      saveBlob(res, `${row.certSerialNumber}.p12`)
     }
   } catch (err) {
     // 用户取消弹框

+ 18 - 10
src/view/sysApplication/pki/rootCaManager/index.vue

@@ -94,14 +94,18 @@
           width="340"
         >
           <template #default="scope">
-            {{ scope.row.validFrom }} ~ {{ scope.row.validTo }}
+            {{ 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"
@@ -137,7 +141,7 @@
             </el-dropdown>
 
             <!-- 冻结 / 解冻:同一按钮,按状态切换 -->
-<!--            <el-button
+            <!--            <el-button
               v-if="scope.row.status === '1'"
               v-preReClick
               link
@@ -446,6 +450,7 @@ const certStatusOptions = [
   { label: '冻结', value: '3' },
   { label: '吊销', value: '4' }
 ]
+const formatCertificateTime = value => value ? String(value).replace('T', ' ') : ''
 const addCertForm = ref({
   certName: '',
   commonName: '',
@@ -540,8 +545,8 @@ const addCertRules = {
       trigger: 'blur'
     }
   ],
-  keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'change' }],
-  keySize: [{ required: true, message: '请选择密钥长度', trigger: 'change' }],
+  keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'blur' }],
+  keySize: [{ required: true, message: '请选择密钥长度', trigger: 'blur' }],
   validityDays: [
     { required: true, message: '请输入证书有效期', trigger: 'blur' },
     {
@@ -593,6 +598,7 @@ const issueCertDialog = () => {
     certUsage: 'ROOT_CA',
     remark: ''
   }
+  addCertFormRef.value?.clearValidate()
   issueCertDialogVisible.value = true
 }
 
@@ -680,7 +686,7 @@ const handleDownload = async(row, format) => {
     let res
     if (format === 'pem') {
       res = await downloadPemCombined({ id: row.id })
-      saveBlob(res, `certificate-${row.id}.pem`)
+      saveBlob(res, `${row.certSerialNumber}.pem`)
     } else if (format === 'p12') {
       // p12 需要密码,弹框让用户输入
       const { value: password } = await ElMessageBox.prompt(
@@ -697,7 +703,7 @@ const handleDownload = async(row, format) => {
         }
       )
       res = await downloadPkcs12({ id: row.id, password })
-      saveBlob(res, `certificate-${row.id}.p12`)
+      saveBlob(res, `${row.certSerialNumber}.p12`)
     }
   } catch (err) {
     // 用户取消弹框
@@ -799,9 +805,11 @@ const handleDelete = async(row) => {
       confirmButtonText: '确定',
       cancelButtonText: '取消'
     })
-    await deleteCertificate(row.id)
-    ElMessage.success('删除成功')
-    await getTableData()
+    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)

+ 111 - 14
src/view/sysApplication/signature/fileConvert/index.vue

@@ -1,6 +1,6 @@
 <script setup>
 import { ref, computed } from 'vue'
-import { pdfToOfd, ofdToPdf } from '@/api/sysApplication/signature' // 根据实际路径调整
+import { checkOfdToPdf, checkPdfFontRisk, pdfToOfd, ofdToPdf } from '@/api/sysApplication/signature'
 
 // ---------- 状态 ----------
 const conversionMode = ref('ofd2pdf') // 'ofd2pdf' | 'pdf2ofd'
@@ -9,6 +9,11 @@ const isConverting = ref(false)
 const message = ref('')
 const messageType = ref('') // '' | 'error' | 'success'
 const uploadRef = ref(null)
+const convertedBlob = ref(null)
+const convertedFilename = ref('')
+const isCheckingRisk = ref(false)
+const riskCheckResult = ref('')
+const maxFileSize = 50 * 1024 * 1024
 
 // ---------- 计算属性 ----------
 const acceptedExtension = computed(() =>
@@ -37,6 +42,11 @@ const resetMessage = () => {
 }
 
 const validateFile = (file) => {
+  if (file.size > maxFileSize) {
+    message.value = '文件大小不能超过 50MB'
+    messageType.value = 'error'
+    return false
+  }
   const ext = file.name.split('.').pop().toLowerCase()
   const expectedExt = conversionMode.value === 'ofd2pdf' ? 'ofd' : 'pdf'
   if (ext !== expectedExt) {
@@ -49,16 +59,56 @@ const validateFile = (file) => {
 
 const handleFileChange = (uploadFile) => {
   const file = uploadFile.raw
+  convertedBlob.value = null
+  convertedFilename.value = ''
+  riskCheckResult.value = ''
   if (!validateFile(file)) {
     uploadRef.value?.clearFiles()
     return
   }
   currentFile.value = file
   resetMessage()
+  checkUploadedFileRisk(file)
+}
+
+const checkUploadedFileRisk = async(file) => {
+  isCheckingRisk.value = true
+  const formData = new FormData()
+  formData.append('file', file)
+
+  try {
+    const checkRisk = conversionMode.value === 'pdf2ofd' ? checkPdfFontRisk : checkOfdToPdf
+    const result = await checkRisk(formData)
+    if (currentFile.value === file) {
+      const riskResult = result?.data || result
+      if (conversionMode.value === 'pdf2ofd') {
+        riskCheckResult.value = riskResult?.safe
+          ? '未发现 PDF 字体风险'
+          : riskResult?.riskMessage || '检测到 PDF 字体风险'
+      } else {
+        riskCheckResult.value = riskResult?.convertible
+          ? '该 OFD 文件可以转换为 PDF'
+          : riskResult?.issues?.join('\n') || '该 OFD 文件无法转换为 PDF'
+      }
+    }
+  } catch (error) {
+    if (currentFile.value === file) {
+      riskCheckResult.value = error.message || '转换风险检查失败'
+    }
+  } finally {
+    if (currentFile.value === file) {
+      isCheckingRisk.value = false
+    }
+  }
 }
 
 const clearFile = () => {
   currentFile.value = null
+  convertedBlob.value = null
+  convertedFilename.value = ''
+  riskCheckResult.value = ''
+  isCheckingRisk.value = false
+  resetMessage()
   uploadRef.value?.clearFiles()
 }
 
@@ -70,16 +120,16 @@ const formatFileSize = (bytes) => {
   return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
 }
 
-// 触发浏览器下载
-const downloadBlob = (blob, filename) => {
-  const url = window.URL.createObjectURL(blob)
+const downloadConvertedFile = () => {
+  if (!convertedBlob.value) return
+  const url = window.URL.createObjectURL(convertedBlob.value)
   const link = document.createElement('a')
   link.href = url
-  link.download = filename
+  link.download = convertedFilename.value
   document.body.appendChild(link)
   link.click()
   document.body.removeChild(link)
-  window.URL.revokeObjectURL(url)
+  window.setTimeout(() => window.URL.revokeObjectURL(url), 60000)
 }
 
 // 从 Content-Disposition 响应头解析文件名
@@ -116,6 +166,8 @@ const startConversion = async() => {
   if (!currentFile.value || isConverting.value) return
 
   isConverting.value = true
+  convertedBlob.value = null
+  convertedFilename.value = ''
   resetMessage()
 
   try {
@@ -125,6 +177,8 @@ const startConversion = async() => {
     const api = conversionMode.value === 'ofd2pdf' ? ofdToPdf : pdfToOfd
     const response = await api(formData)
 
+    const risk = response?.headers?.['x-convert-risk']
+
     // 兼容处理:封装可能返回完整 response,也可能直接返回 blob
     let blob = null
     let headers = {}
@@ -160,11 +214,17 @@ const startConversion = async() => {
     const defaultFilename = `converted.${targetExt}`
     const filename = getFilenameFromHeaders(headers, defaultFilename)
 
-    // 触发下载
-    downloadBlob(blob, filename)
-
-    message.value = `转换成功!已开始下载 ${filename}`
-    messageType.value = 'success'
+    if (risk) {
+      message.value = decodeURIComponent(risk)
+      messageType.value = 'warning'
+      convertedBlob.value = blob
+      convertedFilename.value = filename
+    } else {
+      convertedBlob.value = blob
+      convertedFilename.value = filename
+      message.value = `转换成功:${filename},点击下方按钮下载`
+      messageType.value = 'success'
+    }
   } catch (error) {
     console.error('转换失败:', error)
 
@@ -207,6 +267,7 @@ const startConversion = async() => {
 
     <el-row justify="center">
     <el-space
+      class="file-convert-space"
       fill
     >
       <el-radio-group
@@ -228,12 +289,14 @@ const startConversion = async() => {
         :accept="acceptedExtension"
         :disabled="isConverting"
         :on-change="handleFileChange"
+
       >
         <el-icon><UploadFilled /></el-icon>
         <div class="el-upload__text">
           点击或拖拽 <em>{{ acceptedExtensionText }}</em> 文件到此处
         </div>
         <template #tip>
+          <div class="el-upload__tip">大小限制:50MB</div>
           <div class="el-upload__tip">仅支持单个 {{ acceptedExtensionText }} 文件</div>
         </template>
       </el-upload>
@@ -248,8 +311,7 @@ const startConversion = async() => {
       </el-descriptions>
       <el-button
         v-if="currentFile"
-        type="danger"
-        plain
+        type="primary"
         :disabled="isConverting"
         @click="clearFile"
       >
@@ -258,13 +320,36 @@ const startConversion = async() => {
 
       <el-button
         type="primary"
-        :disabled="!currentFile || isConverting"
+        :disabled="!currentFile || isConverting || isCheckingRisk || (riskCheckResult && riskCheckResult.includes('无法转换'))"
         :loading="isConverting"
         @click="startConversion"
       >
         {{ isConverting ? '转换中…' : `转换为 ${targetFormat}` }}
       </el-button>
 
+      <el-alert
+        v-if="currentFile && isCheckingRisk"
+        title="正在检查转换风险…"
+        type="info"
+        show-icon
+        :closable="false"
+      />
+      <el-card
+        v-if="currentFile && riskCheckResult"
+        shadow="never"
+      >
+        <template #header>转换风险检查结果</template>
+        <pre class="risk-result">{{ riskCheckResult }}</pre>
+      </el-card>
+
+      <el-button
+        v-if="convertedBlob"
+        type="primary"
+        @click="downloadConvertedFile"
+      >
+        下载转换文件
+      </el-button>
+
       <el-alert
         v-if="message"
         :title="message"
@@ -276,3 +361,15 @@ const startConversion = async() => {
     </el-row>
   </el-card>
 </template>
+
+<style scoped>
+.file-convert-space {
+  width: 100%;
+}
+
+.risk-result {
+  margin: 0;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+</style>

+ 8 - 13
src/view/sysApplication/signature/picManager/index.vue

@@ -77,7 +77,11 @@
           label="创建时间"
           prop="createTime"
           width="300"
-        />
+        >
+          <template #default="scope">
+            {{ formatDateTime(scope.row.createTime) }}
+          </template>
+        </el-table-column>
 
         <el-table-column
           fixed="right"
@@ -85,14 +89,12 @@
         >
           <template #default="scope">
             <el-button
-              v-preReClick
               link
               type="primary"
               @click="handleDownload(scope.row)"
             >下载
             </el-button>
             <el-button
-              v-preReClick
               link
               type="primary"
               @click="handleDelete(scope.row)"
@@ -160,6 +162,7 @@
             <el-input-number
               v-model="makeSealForm.sideLength"
               :min="100"
+              :max="1000"
             />
           </el-form-item>
           <!-- 操作 -->
@@ -252,6 +255,8 @@ const getTableData = async () => {
 }
 getTableData()
 
+const formatDateTime = value => value ? String(value).replace('T', ' ') : ''
+
 // 弹窗
 const addPicDialogVisible = ref(false)
 const addPicDialog = () => {
@@ -305,7 +310,6 @@ const makeSealRules = {
   sideLength: [{ required: true, message: '请输入印章边长', trigger: 'blur' }],
 }
 
-
 // 切换类型时清空校验
 const handleTypeChange = async () => {
   await nextTick()
@@ -314,22 +318,13 @@ const handleTypeChange = async () => {
 
 // 生成印模
 const generateSeal = async () => {
-  try {
     await makeSealFormRef.value?.validate()
     const res = await generatePicture(makeSealForm.value)
     if (res.code === 100000) {
       ElMessage.success('生成印模成功')
       addPicDialogVisible.value = false
       await getTableData()
-    } else {
-      ElMessage.error(res.msg || '生成印模失败')
     }
-  } catch (err) {
-    // 校验失败或请求异常
-    if (err?.message) {
-      ElMessage.error(err.message)
-    }
-  }
 }
 
 // 重置表单

+ 69 - 36
src/view/sysApplication/signature/sealManager/index.vue

@@ -78,7 +78,11 @@
           label="创建时间"
           prop="createTime"
           width="200"
-        />
+        >
+          <template #default="scope">
+            {{ formatDateTime(scope.row.createTime) }}
+          </template>
+        </el-table-column>
         <el-table-column
           fixed="right"
           label="操作"
@@ -176,7 +180,7 @@
               />
             </el-select>
           </el-form-item>
-          <el-form-item label="有效期开始">
+<!--          <el-form-item label="有效期开始">
             <el-date-picker
               v-model="createForm.validStartTime"
               type="datetime"
@@ -191,7 +195,7 @@
               value-format="YYYY-MM-DDTHH:mm:ss"
               placeholder="请选择结束时间"
             />
-          </el-form-item>
+          </el-form-item>-->
           <el-form-item label="备注">
             <el-input
               v-model="createForm.remark"
@@ -226,11 +230,11 @@
           <el-descriptions-item label="印章别名">{{ sealDetail.sealAlias }}</el-descriptions-item>
           <el-descriptions-item label="状态"> {{ sealStatusOptions.find(item => item.value === sealDetail.sealStatus)?.label || sealDetail.sealStatus }}</el-descriptions-item>
           <el-descriptions-item label="印模别名">{{ sealDetail.impressionAlias }}</el-descriptions-item>
-          <el-descriptions-item label="数字证书序列号">{{ sealDetail.certAlias }}</el-descriptions-item>
+          <el-descriptions-item label="数字证书别名">{{ sealDetail.certAlias }}</el-descriptions-item>
           <el-descriptions-item label="印章算法">{{ sealDetail.algorithm }}</el-descriptions-item>
-          <el-descriptions-item label="创建时间">{{ sealDetail.createTime }}</el-descriptions-item>
-          <el-descriptions-item label="有效期开始">{{ sealDetail.validStartTime }}</el-descriptions-item>
-          <el-descriptions-item label="有效期结束">{{ sealDetail.validEndTime }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ formatDateTime(sealDetail.createTime) }}</el-descriptions-item>
+          <el-descriptions-item label="有效期开始">{{ formatDateTime(sealDetail.validStartTime) }}</el-descriptions-item>
+          <el-descriptions-item label="有效期结束">{{ formatDateTime(sealDetail.validEndTime) }}</el-descriptions-item>
           <el-descriptions-item
             label="备注"
             :span="2"
@@ -246,10 +250,10 @@
             v-if="!isFrozen && !isDestroyed"
             @click="handleDownload"
           >下载</el-button>
-          <el-button
+<!--          <el-button
             v-if="!isFrozen && !isDestroyed"
             @click="handleExtend"
-          >延期</el-button>
+          >延期</el-button>-->
           <el-button
             v-if="!isDestroyed"
             @click="handleFreeze"
@@ -267,7 +271,7 @@
         </template>
       </el-dialog>
 
-      <el-dialog
+<!--      <el-dialog
         v-model="extendDialogVisible"
         title="印章延期"
         width="460px"
@@ -291,7 +295,7 @@
             @click="submitExtend"
           >确定</el-button>
         </template>
-      </el-dialog>
+      </el-dialog>-->
 
       <el-dialog
         v-model="bindCertDialogVisible"
@@ -384,6 +388,7 @@ const sealStatusOptions = [
   { label: '销毁', value: 3 },
   { label: '过期', value: 4 }
 ]
+const formatDateTime = value => value ? String(value).replace('T', ' ') : ''
 
 const detailDialogVisible = ref(false)
 const sealDetail = ref(null)
@@ -493,11 +498,11 @@ const openDetail = async row => {
 }
 const currentId = () => sealDetail.value?.id
 const handleVerify = async row => {
-  try {
-    const res = await verifySeal(row.id)
-    ElMessage.success(res.message || res.msg || '印章有效')
-  } catch (err) {
-    ElMessage.error(err.message || '印章验证失败')
+  const res = await verifySeal(row.id)
+  if (res.data === true) {
+    ElMessage.success('印章有效')
+  } else {
+    ElMessage.error('印章完整性验证失败,可能遭到篡改')
   }
 }
 const handleDownload = async() => {
@@ -522,18 +527,51 @@ const handleExtend = () => {
 const submitExtend = async() => {
   extendLoading.value = true
   try {
-    const succeeded = await runOperation(() => extendSeal({ sealId: currentId(), newValidEndTime: extendValidEndTime.value }), '印章延期成功')
-    if (succeeded) extendDialogVisible.value = false
+    const response = await extendSeal({ sealId: currentId(), newValidEndTime: extendValidEndTime.value })
+    if (response.code === 100000) {
+      ElMessage.success('印章延期成功')
+      extendDialogVisible.value = false
+      await getTableData()
+      const res = await getSealDetail(currentId())
+      sealDetail.value = res.data
+    }
+  } catch (err) {
+    ElMessage.error(err.message || '印章延期失败')
   } finally {
     extendLoading.value = false
   }
 }
 const handleFreeze = async() => {
-  await runOperation(() => (isFrozen.value ? unfreezeSeal(currentId()) : freezeSeal(currentId())), isFrozen.value ? '印章解冻成功' : '印章冻结成功')
+  try {
+    let response
+    if (isFrozen.value) {
+      response = await unfreezeSeal(currentId())
+    } else {
+      response = await freezeSeal(currentId())
+    }
+    if (response.code === 100000) {
+      ElMessage.success(isFrozen.value ? '印章解冻成功' : '印章冻结成功')
+      await getTableData()
+      const res = await getSealDetail(currentId())
+      sealDetail.value = res.data
+    }
+  } catch (err) {
+    ElMessage.error(err.message || '印章状态更新失败')
+  }
 }
 const handleDestroy = async() => {
-  await ElMessageBox.confirm('销毁后印章将无法恢复,确定继续吗?', '销毁印章', { type: 'warning' })
-  await runOperation(() => destroySeal(currentId()), '印章销毁成功')
+  try {
+    await ElMessageBox.confirm('销毁后印章将无法恢复,确定继续吗?', '销毁印章', { type: 'warning' })
+    const response = await destroySeal(currentId())
+    if (response.code === 100000) {
+      ElMessage.success('印章销毁成功')
+      await getTableData()
+      const res = await getSealDetail(currentId())
+      sealDetail.value = res.data
+    }
+  } catch (err) {
+    if (err !== 'cancel' && err !== 'close') ElMessage.error(err.message || '印章销毁失败')
+  }
 }
 const handleBindCert = async() => {
   try {
@@ -547,25 +585,20 @@ const handleBindCert = async() => {
 const submitBindCert = async() => {
   bindCertLoading.value = true
   try {
-    const succeeded = await runOperation(() => bindSealCert({ sealId: currentId(), certId: bindCertId.value }), '更换绑定证书成功')
-    if (succeeded) bindCertDialogVisible.value = false
+    const response = await bindSealCert({ sealId: currentId(), certId: bindCertId.value })
+    if (response.code === 100000) {
+      ElMessage.success('更换绑定证书成功')
+      bindCertDialogVisible.value = false
+      await getTableData()
+      const res = await getSealDetail(currentId())
+      sealDetail.value = res.data
+    }
+  } catch (err) {
+    ElMessage.error(err.message || '更换绑定证书失败')
   } finally {
     bindCertLoading.value = false
   }
 }
-const runOperation = async(operation, message) => {
-  try {
-    await operation()
-    ElMessage.success(message)
-    await getTableData()
-    const res = await getSealDetail(currentId())
-    sealDetail.value = res.data
-    return true
-  } catch (err) {
-    if (err !== 'cancel' && err !== 'close') ElMessage.error(err.message || '操作失败')
-    return false
-  }
-}
 
 resetCreateForm()
 getTableData()

+ 55 - 10
src/view/sysApplication/signature/signatureManager/index.vue

@@ -12,6 +12,9 @@ const coordinates = ref({ x: 0, y: 0 })
 const signType = ref(0)
 const loadingSeals = ref(false)
 const signing = ref(false)
+const signedFileUrl = ref('')
+const signedFilename = ref('')
+const maxFileSize = 50 * 1024 * 1024
 
 const fileExtension = computed(() => {
   const name = currentFile.value?.name || ''
@@ -46,12 +49,18 @@ const loadSeals = async() => {
 
 const setFile = async(file) => {
   if (!file) return
+  if (file.size > maxFileSize) {
+    ElMessage.error('文件大小不能超过 50MB')
+    return
+  }
   const extension = file.name.split('.').pop()?.toLowerCase()
   if (!['pdf', 'ofd'].includes(extension)) {
     ElMessage.error('仅支持上传 .pdf 或 .ofd 文件')
     return
   }
   currentFile.value = file
+  signedFileUrl.value = ''
+  signedFilename.value = ''
   coordinates.value = { x: 0, y: 0 }
   signType.value = 0
   await loadSeals()
@@ -64,6 +73,8 @@ const handleFileChange = uploadFile => {
 
 const clearFile = () => {
   currentFile.value = null
+  signedFileUrl.value = ''
+  signedFilename.value = ''
   seals.value = []
   selectedSealId.value = null
   uploadRef.value?.clearFiles()
@@ -80,6 +91,8 @@ const startSigning = async() => {
   if (!currentFile.value || !selectedSeal.value || signing.value) return
 
   signing.value = true
+  signedFileUrl.value = ''
+  signedFilename.value = ''
   try {
     const sign = fileExtension.value === 'pdf' ? signPDF : signOFD
     const response = await sign({
@@ -94,24 +107,40 @@ const startSigning = async() => {
     }
 
     const filePrefix = fileExtension.value
-    const signedFileUrl = response.data?.[`${filePrefix}Url`]
-    if (!signedFileUrl) {
+    const downloadUrl = response.data?.[`${filePrefix}Url`]
+    if (!downloadUrl) {
       throw new Error('签章成功,但响应中没有签章文件下载地址')
     }
     const objectName = response.data?.[`${filePrefix}Iden`]?.split('/').pop()
-    const filename = objectName || `${currentFile.value.name.replace(new RegExp(`\\.${filePrefix}$`, 'i'), '')}-signed.${filePrefix}`
+    signedFilename.value = objectName || `${currentFile.value.name.replace(new RegExp(`\\.${filePrefix}$`, 'i'), '')}-signed.${filePrefix}`
+    signedFileUrl.value = downloadUrl
+    ElMessage.success('签章成功')
+  } finally {
+    signing.value = false
+  }
+}
+
+const downloadSignedFile = async () => {
+  if (!signedFileUrl.value) return
+
+  try {
+    const res = await fetch(signedFileUrl.value)
+    const blob = await res.blob()
+    const url = URL.createObjectURL(blob)
+
     const link = document.createElement('a')
-    link.href = signedFileUrl
-    link.download = filename
+    link.href = url
+    link.download = signedFilename.value
     link.rel = 'noopener'
     document.body.appendChild(link)
     link.click()
     link.remove()
-    ElMessage.success('签章成功,文件已开始下载')
+
+    // 释放 blob URL
+    setTimeout(() => URL.revokeObjectURL(url), 60000)
   } catch (error) {
-    ElMessage.error(error.message || '签章失败')
-  } finally {
-    signing.value = false
+    ElMessage.error('下载失败,已尝试直接打开')
+    window.location.href = signedFileUrl.value
   }
 }
 </script>
@@ -127,6 +156,7 @@ const startSigning = async() => {
 
     <el-row justify="center">
     <el-space
+      style="width: 100%;"
       fill
     >
       <el-card shadow="never">
@@ -145,6 +175,9 @@ const startSigning = async() => {
         >
           <el-icon><UploadFilled /></el-icon>
           <div class="el-upload__text">点击或拖拽 PDF / OFD 文件到此处</div>
+          <template #tip>
+            <div class="el-upload__tip">文件大小不能超过 50MB</div>
+          </template>
         </el-upload>
         <el-descriptions
           v-if="currentFile"
@@ -210,8 +243,8 @@ const startSigning = async() => {
       </el-card>
 
       <el-card shadow="never">
+        <el-text type="info">均取值25000时为页面中心</el-text>
         <template #header>3. 签章设置</template>
-        <el-text type="info">tips:均取值25000时为页面中心</el-text>
         <el-form label-position="top">
           <el-row :gutter="20">
             <el-col :xs="24" :sm="12">
@@ -241,12 +274,16 @@ const startSigning = async() => {
             </el-col>
 
           </el-row>
+
+
           <el-form-item label="签章方式">
             <el-radio-group v-model="signType" :disabled="!selectedSeal">
               <el-radio-button :label="0">首页签章</el-radio-button>
               <el-radio-button :label="1">逐页签章</el-radio-button>
             </el-radio-group>
           </el-form-item>
+
+          <el-text type="info">页码大于100页时谨慎选择逐页签章</el-text>
         </el-form>
         <el-button
           type="primary"
@@ -257,6 +294,14 @@ const startSigning = async() => {
           {{ signing ? '签章处理中' : '开始签章' }}
         </el-button>
       </el-card>
+
+      <el-card v-if="signedFileUrl" shadow="never">
+        <template #header>签章文件</template>
+        <el-form-item label="下载地址">
+          <el-input :model-value="signedFileUrl" readonly />
+        </el-form-item>
+        <el-button type="primary" @click="downloadSignedFile">下载签章文件</el-button>
+      </el-card>
     </el-space>
     </el-row>
   </el-card>

+ 42 - 16
src/view/sysApplication/signature/signatureVerify/index.vue

@@ -7,6 +7,7 @@ const uploadRef = ref(null)
 const currentFile = ref(null)
 const verificationResult = ref(null)
 const verifying = ref(false)
+const maxFileSize = 50 * 1024 * 1024
 
 const fileExtension = computed(() => {
   const name = currentFile.value?.name || ''
@@ -18,6 +19,10 @@ const isValid = computed(() => verificationResult.value?.valid === true)
 
 const setFile = file => {
   if (!file) return
+  if (file.size > maxFileSize) {
+    ElMessage.error('文件大小不能超过 50MB')
+    return
+  }
   const extension = file.name.split('.').pop()?.toLowerCase()
   if (!['pdf', 'ofd'].includes(extension)) {
     ElMessage.error('仅支持上传 .pdf 或 .ofd 文件')
@@ -76,23 +81,18 @@ const verifySignature = async() => {
 </script>
 
 <template>
-  <el-card>
+  <el-card >
     <template #header>
       <div>
         <div>验证电子签章</div>
-        <el-text type="info">上传已签章文件,检查签名完整性及签署信息。</el-text>
+        <el-text type="info">上传已签章文件,检查签名完整性及签章信息。</el-text>
       </div>
     </template>
 
-    <el-row justify="center">
-      <el-col
-        :xs="24"
-        :sm="22"
-        :md="20"
-        :lg="18"
-        :xl="16"
-      >
-    <el-card shadow="never">
+    <el-row>
+      <el-col :span="24">
+    <el-card
+      shadow="never">
       <template #header>选择待验证文件</template>
       <el-text type="info">支持 PDF 和 OFD 格式,系统将根据文件后缀自动选择验签方式。</el-text>
       <el-upload
@@ -108,6 +108,9 @@ const verifySignature = async() => {
       >
         <el-icon><UploadFilled /></el-icon>
         <div class="el-upload__text">点击或拖拽 PDF / OFD 文件到此处</div>
+        <template #tip>
+          <div class="el-upload__tip">文件大小不能超过 50MB</div>
+        </template>
       </el-upload>
       <el-descriptions
         v-if="currentFile"
@@ -141,6 +144,7 @@ const verifySignature = async() => {
 
     <el-space
       v-if="verificationResult"
+      class="verification-result"
       direction="vertical"
       fill
     >
@@ -181,12 +185,14 @@ const verifySignature = async() => {
         />
         <el-space
           v-else
+          class="signature-list"
           direction="vertical"
           fill
         >
           <el-card
             v-for="(signature, index) in signatures"
             :key="`${signature.name || 'signature'}-${index}`"
+            class="signature-record"
             shadow="never"
           >
             <template #header>
@@ -198,15 +204,12 @@ const verifySignature = async() => {
               </el-space>
             </template>
             <el-descriptions :column="2" border>
-              <el-descriptions-item label="签署时间">
+              <el-descriptions-item label="签章时间">
                 {{ formatSigningTime(signature.signingTime) }}
               </el-descriptions-item>
-              <el-descriptions-item label="签署人">
+              <el-descriptions-item label="签章人">
                 {{ signature.signerName || '未提供' }}
               </el-descriptions-item>
-              <el-descriptions-item label="文档覆盖范围">
-                {{ signature.coversWholeDocument ? '覆盖整个文档' : '未覆盖整个文档' }}
-              </el-descriptions-item>
               <el-descriptions-item
                 v-if="signature.message"
                 label="验证信息"
@@ -223,3 +226,26 @@ const verifySignature = async() => {
     </el-row>
   </el-card>
 </template>
+
+<style scoped>
+.verification-result {
+  width: 100%;
+  max-width: 100%;
+  box-sizing: border-box;
+}
+
+.signature-record {
+  width: 100%;
+  max-width: 100%;
+  box-sizing: border-box;
+}
+
+.signature-list {
+  display: flex;
+  width: 100%;
+}
+
+.signature-list :deep(.el-space__item) {
+  width: 100%;
+}
+</style>