Bladeren bron

1.签发根证书分离
2.页面样式调整
3.验证电子签章进一步处理

wangkun 3 weken geleden
bovenliggende
commit
51ffc2ec5b

+ 76 - 75
src/view/sysApplication/pki/certManager/index.vue

@@ -53,7 +53,7 @@
           icon="plus"
           type="primary"
           @click="addCertDialog()"
-        >发证
+        >签发证书
         </el-button>
       </div>
       <!-- 表格 -->
@@ -170,7 +170,7 @@
 
       <el-dialog
         v-model="addCertDialogVisible"
-        title="申请根证书"
+        title="签发证书"
         width="50%"
       >
         <el-form
@@ -180,12 +180,45 @@
           label-width="120px"
         >
           <el-form-item
-            label="选择根证书"
+            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
+                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="请选择根证书"
+              placeholder="请选择CA"
             >
               <el-option
                 v-for="item in rootCaList"
@@ -247,7 +280,7 @@
           >
             <el-input
               v-model="addCertForm.state"
-              placeholder="请输入证书ST项"
+              placeholder="请输入证书S项"
             />
           </el-form-item>
           <el-form-item
@@ -259,32 +292,6 @@
               placeholder="请输入证书L项"
             />
           </el-form-item>
-          <el-form-item
-            label="手机号"
-            prop="phone"
-          >
-            <el-input
-              v-model="addCertForm.phone"
-              placeholder="请输入手机号"
-            />
-          </el-form-item>
-
-          <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
             label="证书有效期"
             prop="validityDays"
@@ -295,15 +302,6 @@
               placeholder="请输入证书有效期(天)"
             />
           </el-form-item>
-          <el-form-item
-            label="备注"
-            prop="remark"
-          >
-            <el-input
-              v-model="addCertForm.remark"
-              placeholder="请输入备注"
-            />
-          </el-form-item>
           <el-form-item>
             <el-button
               type="primary"
@@ -333,7 +331,6 @@
 import { ref } from 'vue'
 import {
   addCertificate,
-  addRootCa,
   downloadPemCombined,
   downloadPkcs12,
   freezeCertificate,
@@ -406,32 +403,43 @@ const addCertForm = ref({
   rootCa: '',
   certName: '',
   commonName: '',
-  country: '',
+  country: 'CN',
   organization: '',
   organizationalUnit: '',
   state: '',
   locality: '',
-  phone: '',
   keyAlgorithm: '',
-  keySize: '',
+  keySize: '2048',
   validityDays: '',
   isRoot: '1',
   isCa: '1',
-  certUsage: 'PERSONAL',
-  remark: ''
+  certUsage: 'PERSONAL'
 })
 const addCertRules = {
   certName: [{ required: true, message: '请输入证书名称', trigger: 'blur' }],
   commonName: [{ required: true, message: '请输入证书CN项', trigger: 'blur' }],
   country: [{ required: true, message: '请输入证书C项', trigger: 'blur' }],
-  organization: [{ required: true, message: '请输入证书O项', trigger: 'blur' }],
-  organizationalUnit: [{ required: true, message: '请输入证书OU项', trigger: 'blur' }],
-  state: [{ required: true, message: '请输入证书ST项', trigger: 'blur' }],
-  locality: [{ required: true, message: '请输入证书L项', trigger: 'blur' }],
-  phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+  rootCa: [{ required: true, message: '请选择CA', trigger: 'change' }],
   keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'change' }],
   keySize: [{ required: true, message: '请选择密钥长度', trigger: 'change' }],
-  validityDays: [{ 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 >= 3650 || !Number.isFinite(days)) {
+          callback(new Error('证书有效期必须大于1且小于3650天'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
 }
 
 const addCertDialogVisible = ref(false)
@@ -446,19 +454,17 @@ const addCertDialog = () => {
     rootCa: '',
     certName: '',
     commonName: '',
-    country: '',
+    country: 'CN',
     organization: '',
     organizationalUnit: '',
     state: '',
     locality: '',
-    phone: '',
     keyAlgorithm: '1',
-    keySize: '',
+    keySize: '2048',
     isRoot: '1',
     isCa: '1',
     certUsage: '',
     validityDays: '365',
-    remark: ''
   }
   addCertDialogVisible.value = true
   console.log('addCertDialogVisible', addCertDialogVisible.value)
@@ -466,23 +472,16 @@ const addCertDialog = () => {
 const submitAddCertForm = () => {
   addCertFormRef.value?.validate(async valid => {
     if (!valid) return
-    // 调用接口提交表单数据
-    console.log('提交表单数据', addCertForm.value)
-    if (addCertForm.value.keyAlgorithm === '1') {
-      addCertForm.value.keySize = '2048'
-    } else if (addCertForm.value.keyAlgorithm === '2') {
-      addCertForm.value.keySize = '256'
-    }
-    const isIssuingFromRootCa = Boolean(addCertForm.value.rootCa)
-    addCertForm.value.isRoot = isIssuingFromRootCa ? 0 : 1
-    addCertForm.value.isCa = isIssuingFromRootCa ? 0 : 1
-    addCertForm.value.certUsage = isIssuingFromRootCa ? 'PERSONAL' : 'BOTH'
-    addCertForm.value.issuerId = addCertForm.value.rootCa
-    const res = isIssuingFromRootCa
-      ? await addCertificate(addCertForm.value)
-      : await addRootCa(addCertForm.value)
+    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(isIssuingFromRootCa ? '申请证书成功' : '申请根证书成功')
+      ElMessage.success('申请证书成功')
     } else {
       ElMessage.error(res.msg)
     }
@@ -597,9 +596,11 @@ const handleRenew = async(row) => {
         }
       }
     )
-    await renewCertificate({ id: row.id, validityDays: Number(value) })
-    ElMessage.success('续期成功')
-    getTableData()
+    const res = await renewCertificate({ id: row.id, validityDays: Number(value) })
+    if (res.code === 100000) {
+      ElMessage.success('续期成功')
+      getTableData()
+    }
   } catch (err) {
     if (err === 'cancel' || err === 'close') return
     console.error(err)

+ 162 - 30
src/view/sysApplication/pki/rootCaManager/index.vue

@@ -47,7 +47,15 @@
     </div>
 
     <div class="gva-table-box">
+
       <div class="gva-btn-list">
+        <el-button
+          v-preReClick
+          icon="plus"
+          type="primary"
+          @click="issueCertDialog()"
+        >签发根证书
+        </el-button>
         <el-button
           v-preReClick
           icon="plus"
@@ -199,6 +207,102 @@
           </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 type="primary" @click="submitAddCertForm">提交</el-button>
+        </template>
+      </el-dialog>
+
     </div>
     <!-- 分页栏 -->
     <div class="gva-pagination">
@@ -216,11 +320,12 @@
   </div>
 </template>
 <script setup>
-import { ref, nextTick, onMounted } from 'vue'
+import { ref } from 'vue'
 import { addRootCa, listCertificates, uploadRoot } from '@/api/sysApplication/certificate'
 import { ElMessage } from 'element-plus'
 const searchInfo = ref({})
 const addCertFormRef = ref(null)
+const elSearchFormRef = ref(null)
 // 搜索
 const onSubmit = () => {
   elSearchFormRef.value?.validate(async valid => {
@@ -270,14 +375,14 @@ const certStatusOptions = [
 const addCertForm = ref({
   certName: '',
   commonName: '',
-  country: '',
+  country: 'CN',
   organization: '',
   organizationUnit: '',
   state: '',
   locality: '',
   phone: '',
-  keyAlgorithm: '',
-  keySize: '',
+  keyAlgorithm: '1',
+  keySize: '2048',
   validityDays: '',
   isRoot: '1',
   isCa: '1',
@@ -288,17 +393,30 @@ const addCertRules = {
   certName: [{ required: true, message: '请输入证书名称', trigger: 'blur' }],
   commonName: [{ required: true, message: '请输入证书CN项', trigger: 'blur' }],
   country: [{ required: true, message: '请输入证书C项', trigger: 'blur' }],
-  organization: [{ required: true, message: '请输入证书O项', trigger: 'blur' }],
-  organizationUnit: [{ required: true, message: '请输入证书OU项', trigger: 'blur' }],
-  state: [{ required: true, message: '请输入证书ST项', trigger: 'blur' }],
-  locality: [{ required: true, message: '请输入证书L项', trigger: 'blur' }],
-  phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
   keyAlgorithm: [{ required: true, message: '请选择证书算法', trigger: 'change' }],
   keySize: [{ required: true, message: '请选择密钥长度', trigger: 'change' }],
-  validityDays: [{ 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' }
@@ -310,30 +428,44 @@ 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: ''
+  }
+  issueCertDialogVisible.value = true
+}
+
 const submitAddCertForm = () => {
   addCertFormRef.value?.validate(async valid => {
     if (!valid) return
-    // 调用接口提交表单数据
-    console.log('提交表单数据', addCertForm.value)
-    if (addCertForm.value.keyAlgorithm === '1') {
-      addCertForm.value.keySize = '2048'
-    } else if (addCertForm.value.keyAlgorithm === '2') {
-      addCertForm.value.keySize = '256'
-    }
-    addCertForm.value.isRoot = 1
-    addCertForm.value.isCa = 1
-    addCertForm.value.certUsage = 'BOTH'
-    addRootCa(addCertForm.value).then(res => {
-      if (res.code === 100000) {
-        ElMessage.success('申请根证书成功')
-      } else {
-        ElMessage.error(res.msg)
-      }
+    const res = await addRootCa({
+      ...addCertForm.value,
+      keySize: addCertForm.value.keyAlgorithm === '2' ? '256' : addCertForm.value.keySize,
+      isRoot: 1,
+      isCa: 1,
+      certUsage: 'BOTH'
     })
-    // 关闭对话框
-    addCertDialogVisible.value = false
-    // 刷新表格数据
-    await getTableData()
+    if (res.code === 100000) {
+      ElMessage.success('申请根证书成功')
+      issueCertDialogVisible.value = false
+      await getTableData()
+    } else {
+      ElMessage.error(res.msg)
+    }
   })
 }
 // 文件选择变化

+ 74 - 451
src/view/sysApplication/signature/fileConvert/index.vue

@@ -8,8 +8,7 @@ const currentFile = ref(null)
 const isConverting = ref(false)
 const message = ref('')
 const messageType = ref('') // '' | 'error' | 'success'
-const fileInputRef = ref(null)
-const isDragOver = ref(false)
+const uploadRef = ref(null)
 
 // ---------- 计算属性 ----------
 const acceptedExtension = computed(() =>
@@ -37,11 +36,6 @@ const resetMessage = () => {
   messageType.value = ''
 }
 
-const triggerFileInput = () => {
-  if (isConverting.value) return
-  fileInputRef.value?.click()
-}
-
 const validateFile = (file) => {
   const ext = file.name.split('.').pop().toLowerCase()
   const expectedExt = conversionMode.value === 'ofd2pdf' ? 'ofd' : 'pdf'
@@ -53,22 +47,19 @@ const validateFile = (file) => {
   return true
 }
 
-const handleFileChange = (e) => {
-  const files = e.target.files
-  if (!files || files.length === 0) return
-  const file = files[0]
+const handleFileChange = (uploadFile) => {
+  const file = uploadFile.raw
   if (!validateFile(file)) {
-    e.target.value = ''
+    uploadRef.value?.clearFiles()
     return
   }
   currentFile.value = file
   resetMessage()
-  e.target.value = ''
 }
 
 const clearFile = () => {
   currentFile.value = null
-  if (fileInputRef.value) fileInputRef.value.value = ''
+  uploadRef.value?.clearFiles()
 }
 
 const formatFileSize = (bytes) => {
@@ -203,453 +194,85 @@ const startConversion = async() => {
     isConverting.value = false
   }
 }
-// 拖拽上传
-const handleDragOver = (e) => {
-  e.preventDefault()
-  if (isConverting.value) return
-  isDragOver.value = true
-}
-
-const handleDragLeave = () => {
-  isDragOver.value = false
-}
-
-const handleDrop = (e) => {
-  e.preventDefault()
-  isDragOver.value = false
-  if (isConverting.value) return
-
-  const files = e.dataTransfer.files
-  if (!files || files.length === 0) return
-  const file = files[0]
-  if (!validateFile(file)) return
-  currentFile.value = file
-  resetMessage()
-}
 </script>
 
 <template>
-  <div class="card">
-    <h1>
-      <span class="material-icons">文件转换</span>
-    </h1>
-    <div class="subhead">OFD 与 PDF 互转 · 安全本地处理</div>
-
-    <!-- 转换模式切换 -->
-    <div class="mode-switch">
-      <button
-        class="mode-btn"
-        :class="{ active: conversionMode === 'ofd2pdf' }"
+  <el-card >
+    <template #header>
+      <div>
+        <div>文件转换</div>
+        <el-text type="info">OFD 与 PDF 互转</el-text>
+      </div>
+    </template>
+
+    <el-row justify="center">
+    <el-space
+      fill
+    >
+      <el-radio-group
+        :model-value="conversionMode"
         :disabled="isConverting"
-        @click="setMode('ofd2pdf')"
+        @update:model-value="setMode"
       >
-        <span class="material-icons">OFD → PDF</span>
-      </button>
-      <button
-        class="mode-btn"
-        :class="{ active: conversionMode === 'pdf2ofd' }"
+        <el-radio-button label="ofd2pdf">OFD → PDF</el-radio-button>
+        <el-radio-button label="pdf2ofd">PDF → OFD</el-radio-button>
+      </el-radio-group>
+
+      <el-upload
+        ref="uploadRef"
+        drag
+        action="#"
+        :auto-upload="false"
+        :show-file-list="false"
+        :limit="1"
+        :accept="acceptedExtension"
         :disabled="isConverting"
-        @click="setMode('pdf2ofd')"
+        :on-change="handleFileChange"
       >
-        <span class="material-icons">PDF → OFD</span>
-      </button>
-    </div>
-
-    <!-- 上传区域 -->
-    <div
-      class="upload-area"
-      :class="{ 'drag-over': isDragOver, disabled: isConverting }"
-      @click="triggerFileInput"
-      @dragover="handleDragOver"
-      @dragleave="handleDragLeave"
-      @drop="handleDrop"
-    >
-      <div class="upload-text">点击选择 {{ acceptedExtensionText }} 文件</div>
-      <div class="upload-hint">或拖拽文件到此处(支持单个文件)</div>
-    </div>
-    <input
-      ref="fileInputRef"
-      type="file"
-      :accept="acceptedExtension"
-      style="display: none"
-      @change="handleFileChange"
-    >
+        <el-icon><UploadFilled /></el-icon>
+        <div class="el-upload__text">
+          点击或拖拽 <em>{{ acceptedExtensionText }}</em> 文件到此处
+        </div>
+        <template #tip>
+          <div class="el-upload__tip">仅支持单个 {{ acceptedExtensionText }} 文件</div>
+        </template>
+      </el-upload>
 
-    <!-- 当前文件展示 -->
-    <Transition name="fade">
-      <div
+      <el-descriptions
         v-if="currentFile"
-        class="file-card"
+        :column="2"
+        border
       >
-        <div class="file-info">
-          <div class="file-details">
-            <div class="file-name">{{ currentFile.name }}</div>
-            <div class="file-size">{{ formatFileSize(currentFile.size) }}</div>
-          </div>
-        </div>
-        <button
-          class="remove-btn"
-          title="移除文件"
-          :disabled="isConverting"
-          @click="clearFile"
-        >
-          <span class="material-icons">X</span>
-        </button>
-      </div>
-    </Transition>
-
-    <!-- 转换按钮 -->
-    <button
-      class="convert-btn"
-      :disabled="!currentFile || isConverting"
-      @click="startConversion"
-    >
-      {{ isConverting ? '转换中…' : `转换为 ${targetFormat}` }}
-    </button>
+        <el-descriptions-item label="文件名">{{ currentFile.name }}</el-descriptions-item>
+        <el-descriptions-item label="文件大小">{{ formatFileSize(currentFile.size) }}</el-descriptions-item>
+      </el-descriptions>
+      <el-button
+        v-if="currentFile"
+        type="danger"
+        plain
+        :disabled="isConverting"
+        @click="clearFile"
+      >
+        移除文件
+      </el-button>
+
+      <el-button
+        type="primary"
+        :disabled="!currentFile || isConverting"
+        :loading="isConverting"
+        @click="startConversion"
+      >
+        {{ isConverting ? '转换中…' : `转换为 ${targetFormat}` }}
+      </el-button>
 
-    <!-- 消息反馈 -->
-    <Transition name="fade">
-      <div
+      <el-alert
         v-if="message"
-        class="message-bar"
-        :class="messageType"
-      >
-        {{ message }}
-      </div>
-    </Transition>
-  </div>
+        :title="message"
+        :type="messageType"
+        show-icon
+        :closable="false"
+      />
+    </el-space>
+    </el-row>
+  </el-card>
 </template>
-
-<style scoped lang="scss">
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-  font-family: system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
-}
-
-.card {
-  max-width: 820px;
-  width: 100%;
-  background: rgba(255, 255, 255, 0.85);
-  backdrop-filter: blur(8px);
-  -webkit-backdrop-filter: blur(8px);
-  border-radius: 2.5rem;
-  box-shadow: 0 20px 40px -12px rgba(0, 20, 30, 0.25),
-  0 0 0 1px rgba(255, 255, 255, 0.6) inset;
-  padding: 2rem 2rem 2.2rem;
-  transition: all 0.2s ease;
-  margin: 1.5rem auto;
-}
-
-h1 {
-  font-size: 2rem;
-  font-weight: 600;
-  letter-spacing: -0.02em;
-  color: #0b2b3b;
-  display: flex;
-  align-items: center;
-  gap: 0.6rem;
-  margin-bottom: 0.25rem;
-
-  .material-icons {
-    font-size: 2.2rem;
-    color: #0e6b8c;
-  }
-}
-
-.subhead {
-  font-size: 0.95rem;
-  color: #3e5e6f;
-  margin-bottom: 2rem;
-  border-left: 3px solid #3fa3c7;
-  padding-left: 0.9rem;
-  font-weight: 400;
-}
-
-/* 模式切换 */
-.mode-switch {
-  display: flex;
-  gap: 0.5rem;
-  background: #e2ecf3;
-  padding: 0.4rem;
-  border-radius: 3rem;
-  margin-bottom: 2rem;
-}
-
-.mode-btn {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 0.5rem;
-  padding: 0.8rem;
-  border: none;
-  background: transparent;
-  border-radius: 2.5rem;
-  font-weight: 500;
-  font-size: 1rem;
-  color: #2b4b5c;
-  cursor: pointer;
-  transition: all 0.2s ease;
-  letter-spacing: 0.01em;
-
-  .material-icons {
-    font-size: 1.3rem;
-  }
-
-  &.active {
-    background: white;
-    color: #0f5b7a;
-    box-shadow: 0 6px 14px -6px rgba(0, 60, 90, 0.25),
-    0 0 0 1px rgba(255, 255, 255, 0.8);
-    font-weight: 600;
-  }
-
-  &:not(.active):not(:disabled):hover {
-    background: rgba(255, 255, 255, 0.6);
-  }
-
-  &:disabled {
-    opacity: 0.6;
-    cursor: not-allowed;
-  }
-}
-
-/* 上传区域 */
-.upload-area {
-  border: 2px dashed #b6d3e3;
-  background: #ffffffd6;
-  border-radius: 2rem;
-  padding: 2.2rem 1.5rem;
-  text-align: center;
-  cursor: pointer;
-  transition: all 0.2s ease;
-  margin-bottom: 1.5rem;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 0.6rem;
-
-  &:hover:not(.disabled),
-  &.drag-over {
-    border-color: #1f8db0;
-    background: #f6fdff;
-  }
-
-  &.disabled {
-    opacity: 0.6;
-    cursor: not-allowed;
-  }
-
-  .material-icons {
-    font-size: 3rem;
-    color: #1f8db0;
-    background: #d9effa;
-    padding: 0.7rem;
-    border-radius: 50%;
-    transition: transform 0.1s ease;
-  }
-
-  &:hover:not(.disabled) .material-icons,
-  &.drag-over .material-icons {
-    transform: scale(1.03);
-    background: #c2e5f5;
-  }
-}
-
-.upload-text {
-  font-weight: 500;
-  color: #1a4a5e;
-  font-size: 1.05rem;
-}
-
-.upload-hint {
-  font-size: 0.8rem;
-  color: #6e8f9f;
-}
-
-/* 文件预览卡片 */
-.file-card {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  background: #f4fbfe;
-  border-radius: 1.5rem;
-  padding: 0.9rem 1.3rem;
-  margin-bottom: 1rem;
-  border: 1px solid #cce4f0;
-  transition: all 0.15s ease;
-}
-
-.file-info {
-  display: flex;
-  align-items: center;
-  gap: 0.8rem;
-  overflow: hidden;
-
-  .material-icons {
-    font-size: 1.9rem;
-    color: #247a9e;
-    flex-shrink: 0;
-  }
-}
-
-.file-details {
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-
-.file-name {
-  font-weight: 600;
-  font-size: 1rem;
-  color: #0b2f3f;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  max-width: 300px;
-}
-
-.file-size {
-  font-size: 0.75rem;
-  color: #577e91;
-  margin-top: 0.1rem;
-}
-
-.remove-btn {
-  background: transparent;
-  border: none;
-  color: #7f9baa;
-  border-radius: 50%;
-  width: 36px;
-  height: 36px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  cursor: pointer;
-  transition: all 0.15s ease;
-  flex-shrink: 0;
-
-  &:hover:not(:disabled) {
-    background: #ffe2e2;
-    color: #c13e3e;
-  }
-
-  &:disabled {
-    opacity: 0.4;
-    cursor: not-allowed;
-  }
-}
-
-/* 转换按钮 */
-.convert-btn {
-  width: 100%;
-  border: none;
-  background: linear-gradient(135deg, #126e8f, #1b8fb5);
-  color: white;
-  font-weight: 600;
-  font-size: 1.2rem;
-  padding: 1rem 1.8rem;
-  border-radius: 3rem;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 0.8rem;
-  cursor: pointer;
-  transition: all 0.2s ease;
-  box-shadow: 0 12px 22px -10px #1a6781a0,
-  0 0 0 1px #ffffff40 inset;
-  margin-top: 1.8rem;
-  letter-spacing: 0.01em;
-
-  &:disabled {
-    opacity: 0.45;
-    cursor: not-allowed;
-    box-shadow: none;
-    background: #9bb9c7;
-    transform: none;
-    pointer-events: none;
-  }
-
-  &:not(:disabled):hover {
-    background: linear-gradient(135deg, #0f5c79, #197ea1);
-    transform: translateY(-2px);
-    box-shadow: 0 20px 28px -12px #0b4f66b0,
-    0 0 0 1px #ffffff50 inset;
-  }
-
-  .material-icons {
-    font-size: 1.8rem;
-  }
-}
-
-/* 消息条 */
-.message-bar {
-  margin-top: 1.2rem;
-  padding: 0.8rem 1.2rem;
-  border-radius: 2rem;
-  background: #f0f7fc;
-  font-size: 0.9rem;
-  display: flex;
-  align-items: center;
-  gap: 0.6rem;
-  color: #1e5168;
-  border: 1px solid #b9d9eb;
-  transition: all 0.2s ease;
-  word-break: break-all;
-
-  &.error {
-    background: #ffe9e9;
-    border-color: #f5bcbc;
-    color: #a13030;
-  }
-
-  &.success {
-    background: #e6f6ec;
-    border-color: #b2dfc2;
-    color: #1b6b3c;
-  }
-
-  .material-icons {
-    font-size: 1.3rem;
-    flex-shrink: 0;
-  }
-}
-
-/* 过渡动画 */
-.fade-enter-active,
-.fade-leave-active {
-  transition: opacity 0.2s ease, transform 0.2s ease;
-}
-
-.fade-enter-from,
-.fade-leave-to {
-  opacity: 0;
-  transform: translateY(4px);
-}
-
-/* 响应式 */
-@media (max-width: 500px) {
-  .card {
-    padding: 1.5rem;
-  }
-
-  .file-name {
-    max-width: 160px;
-  }
-
-  .mode-btn {
-    font-size: 0.9rem;
-    padding: 0.6rem 0.4rem;
-
-    .material-icons {
-      font-size: 1.1rem;
-    }
-  }
-
-  .upload-area {
-    padding: 1.5rem 1rem;
-  }
-}
-</style>

+ 37 - 25
src/view/sysApplication/signature/sealManager/index.vue

@@ -116,6 +116,25 @@
           :rules="createRules"
           label-width="110px"
         >
+          <el-form-item
+            label="印章算法"
+            prop="algorithm"
+          >
+            <el-select
+              v-model="createForm.algorithm"
+              placeholder="请选择印章算法"
+              @change="handleAlgorithmChange"
+            >
+              <el-option
+                label="SM2"
+                value="SM2"
+              />
+              <el-option
+                label="RSA"
+                value="RSA"
+              />
+            </el-select>
+          </el-form-item>
           <el-form-item
             label="印章别名"
             prop="sealAlias"
@@ -161,24 +180,6 @@
               />
             </el-select>
           </el-form-item>
-          <el-form-item
-            label="印章算法"
-            prop="algorithm"
-          >
-            <el-select
-              v-model="createForm.algorithm"
-              placeholder="请选择印章算法"
-            >
-              <el-option
-                label="SM2"
-                value="SM2"
-              />
-              <el-option
-                label="RSA"
-                value="RSA"
-              />
-            </el-select>
-          </el-form-item>
           <el-form-item label="有效期开始">
             <el-date-picker
               v-model="createForm.validStartTime"
@@ -409,6 +410,7 @@ const emptyCreateForm = () => ({
 })
 const resetCreateForm = () => {
   createForm.value = emptyCreateForm()
+  certificateOptions.value = []
   createFormRef.value?.clearValidate()
 }
 
@@ -422,16 +424,26 @@ const getTableData = async() => {
   }
 }
 const loadOptions = async() => {
-  const [impressionRes] = await Promise.all([
-    listPicture({ pageNum: 1, pageSize: 1000 }),
-    loadCertificateOptions()
-  ])
+  const impressionRes = await listPicture({ pageNum: 1, pageSize: 1000 })
   impressionOptions.value = impressionRes.data?.list || []
 }
-const loadCertificateOptions = async() => {
-  const res = await listCertificates({ pageNum: 1, pageSize: 1000, certUsage: 'PERSONAL', status: 1 })
+const loadCertificateOptions = async algorithm => {
+  const certAlgo = { RSA: 1, SM2: 2 }[String(algorithm).toUpperCase()]
+  if (!certAlgo) {
+    throw new Error('印章算法无效')
+  }
+  const res = await listCertificates({ pageNum: 1, pageSize: 1000, certUsage: 'PERSONAL', status: 1, certAlgo })
   certificateOptions.value = res.data?.list || []
 }
+const handleAlgorithmChange = async algorithm => {
+  createForm.value.certId = null
+  certificateOptions.value = []
+  try {
+    await loadCertificateOptions(algorithm)
+  } catch (err) {
+    ElMessage.error(err.message || '获取数字证书列表失败')
+  }
+}
 const onSubmit = () => {
   page.value = 1
   getTableData()
@@ -528,7 +540,7 @@ const handleDestroy = async() => {
 }
 const handleBindCert = async() => {
   try {
-    await loadCertificateOptions()
+    await loadCertificateOptions(sealDetail.value?.algorithm)
     bindCertId.value = null
     bindCertDialogVisible.value = true
   } catch (err) {

+ 129 - 524
src/view/sysApplication/signature/signatureManager/index.vue

@@ -4,7 +4,7 @@ import { ElMessage } from 'element-plus'
 import { listSeal } from '@/api/sysApplication/seal'
 import { signOFD, signPDF } from '@/api/sysApplication/signature'
 
-const fileInputRef = ref(null)
+const uploadRef = ref(null)
 const currentFile = ref(null)
 const seals = ref([])
 const selectedSealId = ref(null)
@@ -12,7 +12,6 @@ const coordinates = ref({ x: 0, y: 0 })
 const signType = ref(0)
 const loadingSeals = ref(false)
 const signing = ref(false)
-const isDragOver = ref(false)
 
 const fileExtension = computed(() => {
   const name = currentFile.value?.name || ''
@@ -58,21 +57,16 @@ const setFile = async(file) => {
   await loadSeals()
 }
 
-const handleFileChange = event => {
-  const file = event.target.files?.[0]
-  event.target.value = ''
-  setFile(file)
-}
-
-const handleDrop = event => {
-  isDragOver.value = false
-  setFile(event.dataTransfer.files?.[0])
+const handleFileChange = uploadFile => {
+  uploadRef.value?.clearFiles()
+  setFile(uploadFile.raw)
 }
 
 const clearFile = () => {
   currentFile.value = null
   seals.value = []
   selectedSealId.value = null
+  uploadRef.value?.clearFiles()
 }
 
 const formatFileSize = bytes => {
@@ -123,536 +117,147 @@ const startSigning = async() => {
 </script>
 
 <template>
-  <div class="signature-page">
-    <header class="page-header">
+  <el-card >
+    <template #header>
       <div>
-        <h1>电子签章</h1>
-        <p>上传文件并配置印章、签章位置和签章方式。</p>
+        <div>电子签章</div>
+        <el-text type="info">上传文件并配置印章、签章位置和签章方式。</el-text>
       </div>
-    </header>
-
-    <div class="workflow">
-      <section class="workflow-card">
-        <div class="section-heading">
-          <div class="step-number">01</div>
-          <div>
-            <h2>上传文件</h2>
-            <p>支持 PDF、OFD 格式,文件后缀将决定可用印章算法</p>
-          </div>
-        </div>
-
-        <input
-          ref="fileInputRef"
-          class="file-input"
-          type="file"
+    </template>
+
+    <el-row justify="center">
+    <el-space
+      fill
+    >
+      <el-card shadow="never">
+        <template #header>1. 上传文件</template>
+        <el-text type="info">支持 PDF、OFD 格式,文件后缀将决定可用印章算法。</el-text>
+        <el-upload
+          ref="uploadRef"
+          drag
+          action="#"
+          :auto-upload="false"
+          :show-file-list="false"
+          :limit="1"
           accept=".pdf,.ofd"
-          @change="handleFileChange"
+          :disabled="signing"
+          :on-change="handleFileChange"
         >
-        <div
-          v-if="!currentFile"
-          class="upload-zone"
-          :class="{ 'is-drag-over': isDragOver }"
-          @click="fileInputRef?.click()"
-          @dragover.prevent="isDragOver = true"
-          @dragleave.prevent="isDragOver = false"
-          @drop.prevent="handleDrop"
+          <el-icon><UploadFilled /></el-icon>
+          <div class="el-upload__text">点击或拖拽 PDF / OFD 文件到此处</div>
+        </el-upload>
+        <el-descriptions
+          v-if="currentFile"
+          :column="2"
+          border
         >
-          <div class="upload-icon">
-            <el-icon><UploadFilled /></el-icon>
-          </div>
-          <div class="upload-title">点击选择文件,或拖拽文件到此处</div>
-          <div class="upload-hint">支持 .pdf / .ofd 格式</div>
-          <el-button type="primary" plain class="choose-button">选择文件</el-button>
-        </div>
-
-        <div v-else class="file-card">
-          <div class="file-type" :class="fileExtension">{{ fileExtension.toUpperCase() }}</div>
-          <div class="file-meta">
-            <div class="file-name" :title="currentFile.name">{{ currentFile.name }}</div>
-            <div class="file-subtitle">{{ formatFileSize(currentFile.size) }} · {{ algorithm }} 印章</div>
-          </div>
-          <el-tag type="success" effect="light">已就绪</el-tag>
-          <el-button
-            class="remove-button"
-            text
-            type="danger"
-            @click="clearFile"
-          >移除</el-button>
-        </div>
-      </section>
-
-      <section class="workflow-card" :class="{ 'is-disabled': !currentFile }">
-        <div class="section-heading">
-          <div class="step-number">02</div>
-          <div>
-            <h2>选择印章</h2>
-            <p>{{ currentFile ? `已根据文件类型筛选 ${algorithm} 印章` : '上传文件后自动筛选匹配算法的印章' }}</p>
-          </div>
-          <el-tag v-if="currentFile" effect="plain" type="info">{{ algorithm }}</el-tag>
-        </div>
-
+          <el-descriptions-item label="文件名">{{ currentFile.name }}</el-descriptions-item>
+          <el-descriptions-item label="文件大小">{{ formatFileSize(currentFile.size) }}</el-descriptions-item>
+          <el-descriptions-item label="印章算法">{{ algorithm }}</el-descriptions-item>
+        </el-descriptions>
+        <el-button
+          v-if="currentFile"
+          type="danger"
+          plain
+          :disabled="signing"
+          @click="clearFile"
+        >
+          移除文件
+        </el-button>
+      </el-card>
+
+      <el-card shadow="never">
+        <template #header>
+          <el-space>
+            <span>2. 选择印章</span>
+            <el-tag v-if="currentFile" effect="plain">{{ algorithm }}</el-tag>
+          </el-space>
+        </template>
+        <el-text type="info">
+          {{ currentFile ? `已根据文件类型筛选 ${algorithm} 印章` : '上传文件后自动筛选匹配算法的印章' }}
+        </el-text>
         <el-skeleton v-if="loadingSeals" :rows="2" animated />
-        <template v-else-if="currentFile">
-          <el-empty
-            v-if="seals.length === 0"
-            :image-size="72"
-            description="没有找到匹配算法的可用印章"
-          />
-          <el-radio-group
-            v-else
-            v-model="selectedSealId"
-            class="seal-options"
-          >
+        <el-empty
+          v-else-if="!currentFile"
+          description="请先上传 PDF 或 OFD 文件"
+        />
+        <el-empty
+          v-else-if="seals.length === 0"
+          description="没有找到匹配算法的可用印章"
+        />
+        <el-radio-group
+          v-else
+          style="display: flex;"
+          v-model="selectedSealId"
+        >
+          <el-space wrap>
             <el-radio
               v-for="seal in seals"
               :key="seal.id"
               :label="seal.id"
-              class="seal-option"
               border
             >
-              <span class="seal-mark"><el-icon><Stamp /></el-icon></span>
-              <span class="seal-info">
-                <strong>{{ seal.sealAlias || '未命名印章' }}</strong>
-                <small>{{ seal.impressionAlias || '电子印章' }}<template v-if="seal.certAlias"> · {{ seal.certAlias }}</template></small>
-              </span>
-              <span class="seal-algorithm">{{ seal.algorithm || algorithm }}</span>
-            </el-radio>
-          </el-radio-group>
-        </template>
-        <el-empty
-          v-else
-          :image-size="72"
-          description="请先上传 PDF 或 OFD 文件"
-        />
-        <div v-if="selectedSeal" class="selection-summary">
-          <el-icon><CircleCheckFilled /></el-icon>
-          已选择:{{ selectedSeal.sealAlias }}
-        </div>
-      </section>
-
-      <section class="workflow-card" :class="{ 'is-disabled': !selectedSeal }">
-        <div class="section-heading">
-          <div class="step-number">03</div>
-          <div>
-            <h2>签章设置</h2>
-            <p>设置签章坐标及应用范围</p>
-          </div>
-        </div>
-
-        <el-form label-position="top" class="settings-form">
-          <div class="coordinate-fields">
-            <el-form-item label="X 坐标">
-              <el-input-number
-                v-model="coordinates.x"
-                :min="0"
-                :precision="2"
-                :step="1"
-                controls-position="right"
-                :disabled="!selectedSeal"
-              />
-            </el-form-item>
-            <el-form-item label="Y 坐标">
-              <el-input-number
-                v-model="coordinates.y"
-                :min="0"
-                :precision="2"
-                :step="1"
-                controls-position="right"
-                :disabled="!selectedSeal"
-              />
-            </el-form-item>
-          </div>
+              <el-space direction="vertical" alignment="start">
 
+                <el-text type="info">
+                  {{ seal.impressionAlias || '电子印章' }}<template v-if="seal.certAlias"> · {{ seal.certAlias }}</template>
+                </el-text>
+               </el-space>
+            </el-radio>
+          </el-space>
+        </el-radio-group>
+
+      </el-card>
+
+      <el-card shadow="never">
+        <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">
+              <el-form-item label="X 坐标">
+                <el-input-number
+                  v-model="coordinates.x"
+                  :min="0"
+                  :precision="0"
+                  :step="1"
+                  controls-position="right"
+                  :disabled="!selectedSeal"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="Y 坐标">
+                <el-input-number
+                  v-model="coordinates.y"
+                  :min="0"
+                  :precision="0"
+                  :step="1"
+                  controls-position="right"
+                  :disabled="!selectedSeal"
+                />
+              </el-form-item>
+
+            </el-col>
+
+          </el-row>
           <el-form-item label="签章方式">
-            <el-radio-group v-model="signType" :disabled="!selectedSeal" class="sign-type-options">
+            <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-form>
-
-        <div class="workflow-footer">
-          <span>配置完成后即可发起签章</span>
-          <el-button
-            type="primary"
-            :loading="signing"
-            :disabled="!currentFile || !selectedSeal || signing"
-            @click="startSigning"
-          >
-            <el-icon><EditPen /></el-icon>
-            {{ signing ? '签章处理中' : '开始签章' }}
-          </el-button>
-        </div>
-      </section>
-    </div>
-  </div>
+        <el-button
+          type="primary"
+          :loading="signing"
+          :disabled="!currentFile || !selectedSeal || signing"
+          @click="startSigning"
+        >
+          {{ signing ? '签章处理中' : '开始签章' }}
+        </el-button>
+      </el-card>
+    </el-space>
+    </el-row>
+  </el-card>
 </template>
-
-<style scoped lang="scss">
-.signature-page {
-  max-width: 1080px;
-  margin: 0 auto;
-  padding: 28px 24px 44px;
-  color: #202b3c;
-}
-
-.page-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 24px;
-
-  h1 {
-    margin: 5px 0 6px;
-    font-size: 26px;
-    font-weight: 650;
-    letter-spacing: -0.4px;
-  }
-
-  p {
-    margin: 0;
-    color: #8792a2;
-    font-size: 14px;
-  }
-}
-
-.eyebrow {
-  color: #4779dc;
-  font-size: 10px;
-  font-weight: 700;
-  letter-spacing: 1.5px;
-}
-
-.secure-badge {
-  display: flex;
-  align-items: center;
-  gap: 7px;
-  padding: 8px 12px;
-  border: 1px solid #dce9df;
-  border-radius: 20px;
-  background: #f5fbf6;
-  color: #4b9664;
-  font-size: 12px;
-}
-
-.workflow {
-  display: grid;
-  gap: 16px;
-}
-
-.workflow-card {
-  padding: 22px 24px;
-  border: 1px solid #e8edf3;
-  border-radius: 10px;
-  background: #fff;
-  box-shadow: 0 3px 12px rgb(25 50 85 / 3%);
-  transition: opacity 0.2s ease;
-
-  &.is-disabled {
-    opacity: 0.72;
-  }
-}
-
-.section-heading {
-  display: flex;
-  align-items: center;
-  gap: 13px;
-  margin-bottom: 20px;
-
-  h2 {
-    margin: 0 0 5px;
-    color: #263449;
-    font-size: 16px;
-    font-weight: 600;
-  }
-
-  p {
-    margin: 0;
-    color: #98a2b1;
-    font-size: 12px;
-  }
-
-  > .el-tag {
-    margin-left: auto;
-  }
-}
-
-.step-number {
-  display: grid;
-  width: 34px;
-  height: 34px;
-  flex: 0 0 34px;
-  place-items: center;
-  border-radius: 9px;
-  background: #edf3ff;
-  color: #4779dc;
-  font-size: 12px;
-  font-weight: 700;
-}
-
-.file-input {
-  display: none;
-}
-
-.upload-zone {
-  display: flex;
-  min-height: 190px;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  border: 1px dashed #c9d5e4;
-  border-radius: 8px;
-  background: #fbfcfe;
-  cursor: pointer;
-  transition: all 0.2s ease;
-
-  &:hover,
-  &.is-drag-over {
-    border-color: #5788e5;
-    background: #f5f8ff;
-  }
-}
-
-.upload-icon {
-  display: grid;
-  width: 42px;
-  height: 42px;
-  place-items: center;
-  margin-bottom: 10px;
-  border-radius: 50%;
-  background: #edf3ff;
-  color: #5684dc;
-  font-size: 20px;
-}
-
-.upload-title {
-  color: #46556b;
-  font-size: 13px;
-}
-
-.upload-hint {
-  margin: 6px 0 12px;
-  color: #a0a9b7;
-  font-size: 12px;
-}
-
-.choose-button {
-  height: 30px;
-  padding: 0 14px;
-}
-
-.file-card {
-  display: flex;
-  align-items: center;
-  gap: 13px;
-  min-height: 76px;
-  padding: 13px 15px;
-  border: 1px solid #e7edf5;
-  border-radius: 8px;
-  background: #fbfcfe;
-}
-
-.file-type {
-  display: grid;
-  width: 42px;
-  height: 48px;
-  flex: 0 0 42px;
-  place-items: center;
-  border-radius: 6px;
-  background: #fff0ee;
-  color: #db675c;
-  font-size: 10px;
-  font-weight: 700;
-
-  &.ofd {
-    background: #eff3ff;
-    color: #5877c5;
-  }
-}
-
-.file-meta {
-  min-width: 0;
-  flex: 1;
-}
-
-.file-name {
-  overflow: hidden;
-  color: #344258;
-  font-size: 13px;
-  font-weight: 600;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.file-subtitle {
-  margin-top: 6px;
-  color: #97a1b0;
-  font-size: 11px;
-}
-
-.remove-button {
-  margin-left: 4px;
-}
-
-.seal-options {
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 10px;
-  width: 100%;
-}
-
-.seal-option {
-  display: flex;
-  width: 100%;
-  height: auto;
-  min-height: 68px;
-  margin: 0;
-  padding: 12px;
-  border-color: #e5eaf1;
-  border-radius: 8px;
-
-  :deep(.el-radio__label) {
-    display: flex;
-    min-width: 0;
-    flex: 1;
-    align-items: center;
-    gap: 10px;
-    padding-left: 8px;
-  }
-
-  &.is-checked {
-    border-color: #76a0ee;
-    background: #f7f9ff;
-  }
-}
-
-.seal-mark {
-  display: grid;
-  width: 34px;
-  height: 34px;
-  flex: 0 0 34px;
-  place-items: center;
-  border-radius: 8px;
-  background: #fff2f0;
-  color: #d96d62;
-  font-size: 18px;
-}
-
-.seal-info {
-  display: flex;
-  min-width: 0;
-  flex: 1;
-  flex-direction: column;
-  gap: 5px;
-
-  strong {
-    overflow: hidden;
-    color: #38465b;
-    font-size: 12px;
-    font-weight: 600;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-
-  small {
-    overflow: hidden;
-    color: #9aa4b2;
-    font-size: 10px;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-}
-
-.seal-algorithm {
-  color: #8994a3;
-  font-size: 10px;
-}
-
-.selection-summary {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  margin-top: 14px;
-  color: #459266;
-  font-size: 12px;
-}
-
-.settings-form {
-  max-width: 620px;
-  padding-left: 47px;
-
-  :deep(.el-form-item__label) {
-    padding-bottom: 6px;
-    color: #59667a;
-    font-size: 12px;
-  }
-}
-
-.coordinate-fields {
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 18px;
-
-  .el-input-number {
-    width: 100%;
-  }
-}
-
-.field-hint {
-  display: block;
-  margin-top: 6px;
-  color: #a0a9b6;
-  font-size: 10px;
-}
-
-.sign-type-options {
-  :deep(.el-radio-button__inner) {
-    padding: 9px 20px;
-  }
-}
-
-.workflow-footer {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin: 7px 0 0 47px;
-  padding-top: 17px;
-  border-top: 1px solid #f0f2f5;
-
-  > span {
-    color: #9aa4b2;
-    font-size: 11px;
-  }
-
-  .el-button {
-    gap: 6px;
-  }
-}
-
-@media (max-width: 680px) {
-  .signature-page {
-    padding: 20px 14px 32px;
-  }
-
-  .workflow-card {
-    padding: 18px 16px;
-  }
-
-  .seal-options,
-  .coordinate-fields {
-    grid-template-columns: 1fr;
-  }
-
-  .settings-form,
-  .workflow-footer {
-    margin-left: 0;
-    padding-left: 0;
-  }
-
-  .workflow-footer {
-    align-items: flex-start;
-    flex-direction: column;
-    gap: 12px;
-  }
-}
-</style>

+ 125 - 567
src/view/sysApplication/signature/signatureVerify/index.vue

@@ -3,11 +3,10 @@ import { computed, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { verifyOFDSignature, verifyPDFSignature } from '@/api/sysApplication/signature'
 
-const fileInputRef = ref(null)
+const uploadRef = ref(null)
 const currentFile = ref(null)
 const verificationResult = ref(null)
 const verifying = ref(false)
-const isDragOver = ref(false)
 
 const fileExtension = computed(() => {
   const name = currentFile.value?.name || ''
@@ -28,20 +27,15 @@ const setFile = file => {
   verificationResult.value = null
 }
 
-const handleFileChange = event => {
-  const file = event.target.files?.[0]
-  event.target.value = ''
-  setFile(file)
-}
-
-const handleDrop = event => {
-  isDragOver.value = false
-  setFile(event.dataTransfer.files?.[0])
+const handleFileChange = uploadFile => {
+  uploadRef.value?.clearFiles()
+  setFile(uploadFile.raw)
 }
 
 const clearFile = () => {
   currentFile.value = null
   verificationResult.value = null
+  uploadRef.value?.clearFiles()
 }
 
 const formatFileSize = bytes => {
@@ -82,586 +76,150 @@ const verifySignature = async() => {
 </script>
 
 <template>
-  <div class="verify-page">
-    <header class="page-header">
+  <el-card>
+    <template #header>
       <div>
-        <h1>验证电子签章</h1>
-        <p>上传已签章文件,检查签名完整性及签署信息。</p>
+        <div>验证电子签章</div>
+        <el-text type="info">上传已签章文件,检查签名完整性及签署信息。</el-text>
       </div>
-    </header>
-
-    <section class="content-card">
-      <div class="section-heading">
-        <div class="step-number">01</div>
-        <div>
-          <h2>选择待验证文件</h2>
-          <p>支持 PDF 和 OFD 格式,系统将根据文件后缀自动选择验签方式</p>
-        </div>
-      </div>
-
-      <input
-        ref="fileInputRef"
-        class="file-input"
-        type="file"
+    </template>
+
+    <el-row justify="center">
+      <el-col
+        :xs="24"
+        :sm="22"
+        :md="20"
+        :lg="18"
+        :xl="16"
+      >
+    <el-card shadow="never">
+      <template #header>选择待验证文件</template>
+      <el-text type="info">支持 PDF 和 OFD 格式,系统将根据文件后缀自动选择验签方式。</el-text>
+      <el-upload
+        ref="uploadRef"
+        drag
+        action="#"
+        :auto-upload="false"
+        :show-file-list="false"
+        :limit="1"
         accept=".pdf,.ofd"
-        @change="handleFileChange"
+        :disabled="verifying"
+        :on-change="handleFileChange"
       >
-      <div
-        v-if="!currentFile"
-        class="upload-zone"
-        :class="{ 'is-drag-over': isDragOver }"
-        @click="fileInputRef?.click()"
-        @dragover.prevent="isDragOver = true"
-        @dragleave.prevent="isDragOver = false"
-        @drop.prevent="handleDrop"
+        <el-icon><UploadFilled /></el-icon>
+        <div class="el-upload__text">点击或拖拽 PDF / OFD 文件到此处</div>
+      </el-upload>
+      <el-descriptions
+        v-if="currentFile"
+        :column="2"
+        border
       >
-        <div class="upload-icon">
-          <el-icon><UploadFilled /></el-icon>
-        </div>
-        <div class="upload-title">点击选择文件,或拖拽文件到此处</div>
-        <div class="upload-hint">支持 .pdf / .ofd 格式</div>
-        <el-button type="primary" plain class="choose-button">选择文件</el-button>
-      </div>
-
-      <div v-else class="file-card">
-        <div class="file-type" :class="fileExtension">{{ fileFormat }}</div>
-        <div class="file-meta">
-          <div class="file-name" :title="currentFile.name">{{ currentFile.name }}</div>
-          <div class="file-subtitle">{{ formatFileSize(currentFile.size) }} · {{ fileFormat }} 文件</div>
-        </div>
-        <el-tag type="success" effect="light">已就绪</el-tag>
+        <el-descriptions-item label="文件名">{{ currentFile.name }}</el-descriptions-item>
+        <el-descriptions-item label="文件大小">{{ formatFileSize(currentFile.size) }}</el-descriptions-item>
+        <el-descriptions-item label="文件格式">{{ fileFormat }}</el-descriptions-item>
+      </el-descriptions>
+      <el-space>
         <el-button
-          class="remove-button"
-          text
+          v-if="currentFile"
           type="danger"
+          plain
           :disabled="verifying"
           @click="clearFile"
-        >移除</el-button>
-      </div>
-
-      <div class="action-row">
-        <span class="action-hint">
-          <el-icon><InfoFilled /></el-icon>
-          验签结果将展示签名有效性、签章数量及签署人信息
-        </span>
+        >
+          移除文件
+        </el-button>
         <el-button
           type="primary"
           :loading="verifying"
           :disabled="!currentFile || verifying"
           @click="verifySignature"
         >
-          <el-icon v-if="!verifying"><CircleCheck /></el-icon>
           {{ verifying ? '正在验证' : '开始验证' }}
         </el-button>
-      </div>
-    </section>
-
-    <section v-if="verificationResult" class="result-section">
-      <div class="result-banner" :class="isValid ? 'is-valid' : 'is-invalid'">
-        <div class="result-icon">
-          <el-icon><CircleCheckFilled v-if="isValid" /><CircleCloseFilled v-else /></el-icon>
-        </div>
-        <div class="result-summary">
-          <h2>{{ isValid ? '签名验证通过' : '签名验证未通过' }}</h2>
-          <p>{{ verificationResult.message || '验签完成' }}</p>
-        </div>
-        <div class="result-count">
-          <strong>{{ verificationResult.signatureCount ?? signatures.length }}</strong>
-          <span>个签名</span>
-        </div>
-      </div>
-
-      <div class="result-metrics">
-        <div class="metric-card">
-          <span class="metric-label">文件格式</span>
-          <strong>{{ fileFormat }}</strong>
-        </div>
-        <div class="metric-card">
-          <span class="metric-label">签名数量</span>
-          <strong>{{ verificationResult.signatureCount ?? signatures.length }}</strong>
-        </div>
-        <div class="metric-card">
-          <span class="metric-label">信任链检查</span>
-          <el-tag :type="verificationResult.trustChecked ? 'success' : 'info'" effect="light">
+      </el-space>
+    </el-card>
+
+    <el-space
+      v-if="verificationResult"
+      direction="vertical"
+      fill
+    >
+      <el-alert
+        :title="isValid ? '签名验证通过' : '签名验证未通过'"
+        :description="verificationResult.message || '验签完成'"
+        :type="isValid ? 'success' : 'error'"
+        show-icon
+        :closable="false"
+      />
+
+      <el-descriptions
+        title="验证结果"
+        :column="3"
+        border
+      >
+        <el-descriptions-item label="文件格式">{{ fileFormat }}</el-descriptions-item>
+        <el-descriptions-item label="签名数量">
+          {{ verificationResult.signatureCount ?? signatures.length }}
+        </el-descriptions-item>
+        <el-descriptions-item label="信任链检查">
+          <el-tag :type="verificationResult.trustChecked ? 'success' : 'info'">
             {{ verificationResult.trustChecked ? '已检查' : '未检查' }}
           </el-tag>
-        </div>
-      </div>
-
-      <div class="signatures-card">
-        <div class="signatures-heading">
-          <div>
-            <h2>签名详情</h2>
-            <p>签名完整性与签署者信息</p>
-          </div>
-          <el-tag effect="plain" type="info">{{ signatures.length }} 项</el-tag>
-        </div>
-
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <el-card shadow="never">
+        <template #header>
+          <el-space>
+            <span>签名详情</span>
+            <el-tag effect="plain" type="info">{{ signatures.length }} 项</el-tag>
+          </el-space>
+        </template>
         <el-empty
           v-if="!signatures.length"
-          :image-size="72"
           description="文件中没有签名信息"
         />
-        <div v-else class="signature-list">
-          <article
+        <el-space
+          v-else
+          direction="vertical"
+          fill
+        >
+          <el-card
             v-for="(signature, index) in signatures"
             :key="`${signature.name || 'signature'}-${index}`"
-            class="signature-item"
+            shadow="never"
           >
-            <div class="signature-item-heading">
-              <div class="signature-number">{{ String(index + 1).padStart(2, '0') }}</div>
-              <div class="signature-name">
-                <strong>{{ signature.name || `签名 ${index + 1}` }}</strong>
-                <span>{{ formatSigningTime(signature.signingTime) }}</span>
-              </div>
-              <el-tag :type="signature.valid ? 'success' : 'danger'" effect="light">
-                {{ signature.valid ? '有效' : '无效' }}
-              </el-tag>
-            </div>
-            <div class="signature-details">
-              <div class="detail-cell">
-                <span>签署人</span>
-                <strong>{{ signature.signerName || '未提供' }}</strong>
-              </div>
-              <div class="detail-cell">
-                <span>文档覆盖范围</span>
-                <strong>{{ signature.coversWholeDocument ? '覆盖整个文档' : '未覆盖整个文档' }}</strong>
-              </div>
-            </div>
-          </article>
-        </div>
-      </div>
-    </section>
-  </div>
+            <template #header>
+              <el-space>
+                <span>{{ signature.name || `签名 ${index + 1}` }}</span>
+                <el-tag :type="signature.valid ? 'success' : 'danger'">
+                  {{ signature.valid ? '有效' : '无效' }}
+                </el-tag>
+              </el-space>
+            </template>
+            <el-descriptions :column="2" border>
+              <el-descriptions-item label="签署时间">
+                {{ formatSigningTime(signature.signingTime) }}
+              </el-descriptions-item>
+              <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="验证信息"
+                :span="2"
+              >
+                {{ signature.message }}
+              </el-descriptions-item>
+            </el-descriptions>
+          </el-card>
+        </el-space>
+      </el-card>
+    </el-space>
+      </el-col>
+    </el-row>
+  </el-card>
 </template>
-
-<style scoped lang="scss">
-.verify-page {
-  max-width: 1080px;
-  margin: 0 auto;
-  padding: 28px 24px 44px;
-  color: #202b3c;
-}
-
-.page-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 24px;
-
-  h1 {
-    margin: 5px 0 6px;
-    font-size: 26px;
-    font-weight: 650;
-    letter-spacing: -0.4px;
-  }
-
-  p {
-    margin: 0;
-    color: #8792a2;
-    font-size: 14px;
-  }
-}
-
-.eyebrow {
-  color: #4779dc;
-  font-size: 10px;
-  font-weight: 700;
-  letter-spacing: 1.5px;
-}
-
-.secure-badge {
-  display: flex;
-  align-items: center;
-  gap: 7px;
-  padding: 8px 12px;
-  border: 1px solid #dce9df;
-  border-radius: 20px;
-  background: #f5fbf6;
-  color: #4b9664;
-  font-size: 12px;
-}
-
-.content-card,
-.signatures-card {
-  padding: 22px 24px;
-  border: 1px solid #e8edf3;
-  border-radius: 10px;
-  background: #fff;
-  box-shadow: 0 3px 12px rgb(25 50 85 / 3%);
-}
-
-.section-heading,
-.signatures-heading {
-  display: flex;
-  align-items: center;
-  gap: 13px;
-  margin-bottom: 20px;
-
-  h2 {
-    margin: 0 0 5px;
-    color: #263449;
-    font-size: 16px;
-    font-weight: 600;
-  }
-
-  p {
-    margin: 0;
-    color: #98a2b1;
-    font-size: 12px;
-  }
-}
-
-.step-number {
-  display: grid;
-  width: 34px;
-  height: 34px;
-  flex: 0 0 34px;
-  place-items: center;
-  border-radius: 9px;
-  background: #edf3ff;
-  color: #4779dc;
-  font-size: 12px;
-  font-weight: 700;
-}
-
-.file-input {
-  display: none;
-}
-
-.upload-zone {
-  display: flex;
-  min-height: 190px;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  border: 1px dashed #c9d5e4;
-  border-radius: 8px;
-  background: #fbfcfe;
-  cursor: pointer;
-  transition: all 0.2s ease;
-
-  &:hover,
-  &.is-drag-over {
-    border-color: #5788e5;
-    background: #f5f8ff;
-  }
-}
-
-.upload-icon {
-  display: grid;
-  width: 42px;
-  height: 42px;
-  place-items: center;
-  margin-bottom: 10px;
-  border-radius: 50%;
-  background: #edf3ff;
-  color: #5684dc;
-  font-size: 20px;
-}
-
-.upload-title {
-  color: #46556b;
-  font-size: 13px;
-}
-
-.upload-hint {
-  margin: 6px 0 12px;
-  color: #a0a9b7;
-  font-size: 12px;
-}
-
-.choose-button {
-  height: 30px;
-  padding: 0 14px;
-}
-
-.file-card {
-  display: flex;
-  align-items: center;
-  gap: 13px;
-  min-height: 76px;
-  padding: 13px 15px;
-  border: 1px solid #e7edf5;
-  border-radius: 8px;
-  background: #fbfcfe;
-}
-
-.file-type {
-  display: grid;
-  width: 42px;
-  height: 48px;
-  flex: 0 0 42px;
-  place-items: center;
-  border-radius: 6px;
-  background: #fff0ee;
-  color: #db675c;
-  font-size: 10px;
-  font-weight: 700;
-
-  &.ofd {
-    background: #eff3ff;
-    color: #5877c5;
-  }
-}
-
-.file-meta {
-  min-width: 0;
-  flex: 1;
-}
-
-.file-name {
-  overflow: hidden;
-  color: #344258;
-  font-size: 13px;
-  font-weight: 600;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.file-subtitle {
-  margin-top: 6px;
-  color: #97a1b0;
-  font-size: 11px;
-}
-
-.remove-button {
-  margin-left: 4px;
-}
-
-.action-row {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 16px;
-  margin-top: 18px;
-}
-
-.action-hint {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  color: #98a2b1;
-  font-size: 11px;
-}
-
-.result-section {
-  display: grid;
-  gap: 16px;
-  margin-top: 18px;
-}
-
-.result-banner {
-  display: flex;
-  align-items: center;
-  gap: 14px;
-  padding: 18px 20px;
-  border: 1px solid;
-  border-radius: 10px;
-
-  &.is-valid {
-    border-color: #d9ebde;
-    background: #f5fbf6;
-    color: #3b8b59;
-  }
-
-  &.is-invalid {
-    border-color: #f2dbd8;
-    background: #fff8f7;
-    color: #c85e54;
-  }
-}
-
-.result-icon {
-  font-size: 28px;
-}
-
-.result-summary {
-  min-width: 0;
-  flex: 1;
-
-  h2 {
-    margin: 0 0 5px;
-    font-size: 16px;
-    font-weight: 600;
-  }
-
-  p {
-    margin: 0;
-    font-size: 12px;
-    opacity: 0.82;
-  }
-}
-
-.result-count {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding-left: 18px;
-  border-left: 1px solid currentColor;
-  opacity: 0.85;
-
-  strong {
-    font-size: 20px;
-    line-height: 1.2;
-  }
-
-  span {
-    margin-top: 3px;
-    font-size: 10px;
-  }
-}
-
-.result-metrics {
-  display: grid;
-  grid-template-columns: repeat(3, minmax(0, 1fr));
-  gap: 12px;
-}
-
-.metric-card {
-  display: flex;
-  min-height: 80px;
-  flex-direction: column;
-  align-items: flex-start;
-  justify-content: center;
-  gap: 9px;
-  padding: 14px 17px;
-  border: 1px solid #e8edf3;
-  border-radius: 9px;
-  background: #fff;
-
-  strong {
-    color: #35445a;
-    font-size: 15px;
-    font-weight: 600;
-  }
-
-  .el-tag {
-    height: 22px;
-  }
-}
-
-.metric-label {
-  color: #98a2b1;
-  font-size: 11px;
-}
-
-.signatures-card {
-  padding-bottom: 10px;
-}
-
-.signatures-heading {
-  justify-content: space-between;
-  margin-bottom: 17px;
-}
-
-.signature-list {
-  display: grid;
-  gap: 10px;
-}
-
-.signature-item {
-  padding: 15px 16px;
-  border: 1px solid #edf0f4;
-  border-radius: 8px;
-  background: #fcfdff;
-}
-
-.signature-item-heading {
-  display: flex;
-  align-items: center;
-  gap: 11px;
-}
-
-.signature-number {
-  display: grid;
-  width: 30px;
-  height: 30px;
-  flex: 0 0 30px;
-  place-items: center;
-  border-radius: 7px;
-  background: #edf3ff;
-  color: #4779dc;
-  font-size: 10px;
-  font-weight: 700;
-}
-
-.signature-name {
-  display: flex;
-  min-width: 0;
-  flex: 1;
-  flex-direction: column;
-  gap: 4px;
-
-  strong {
-    overflow: hidden;
-    color: #38465b;
-    font-size: 12px;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-
-  span {
-    color: #9aa4b2;
-    font-size: 10px;
-  }
-}
-
-.signature-details {
-  display: grid;
-  grid-template-columns: minmax(0, 2fr) minmax(130px, 1fr);
-  gap: 15px;
-  margin: 15px 0 0 41px;
-  padding-top: 13px;
-  border-top: 1px solid #edf0f4;
-}
-
-.detail-cell {
-  display: flex;
-  min-width: 0;
-  flex-direction: column;
-  gap: 6px;
-
-  span {
-    color: #9aa4b2;
-    font-size: 10px;
-  }
-
-  strong {
-    overflow-wrap: anywhere;
-    color: #58667a;
-    font-size: 11px;
-    font-weight: 500;
-  }
-}
-
-@media (max-width: 680px) {
-  .verify-page {
-    padding: 20px 14px 32px;
-  }
-
-  .content-card,
-  .signatures-card {
-    padding: 18px 16px;
-  }
-
-  .secure-badge,
-  .action-hint {
-    display: none;
-  }
-
-  .result-metrics {
-    grid-template-columns: 1fr;
-    gap: 8px;
-  }
-
-  .metric-card {
-    min-height: 66px;
-  }
-
-  .signature-details {
-    grid-template-columns: 1fr;
-    margin-left: 0;
-  }
-}
-</style>