Kaynağa Gözat

feat:1.修改平台名称
2.修改快捷入口
3.修改md5和维吉尼亚密码的页面

wangkun 6 gün önce
ebeveyn
işleme
277d3f2c06

+ 4 - 0
package.json

@@ -21,6 +21,7 @@
     "@wangeditor/editor-for-vue": "^5.1.12",
     "axios": "^1.4.0",
     "core-js": "^3.31.1",
+    "crypto-js": "^4.2.0",
     "dayjs": "^1.11.13",
     "echarts": "^5.4.3",
     "editor.md": "^1.5.0",
@@ -29,7 +30,9 @@
     "js-cookie": "^3.0.5",
     "lodash": "^4.17.21",
     "marked": "4.3.0",
+    "mermaid": "^12.2.0",
     "mitt": "^3.0.1",
+    "node-forge": "^1.4.0",
     "nprogress": "^0.2.0",
     "path": "^0.12.7",
     "pinia": "^2.1.4",
@@ -38,6 +41,7 @@
     "qrcode": "^1.5.4",
     "qs": "^6.11.2",
     "screenfull": "^6.0.2",
+    "sm-crypto": "^0.5.7",
     "spark-md5": "^3.0.2",
     "tailwindcss": "^3.3.3",
     "uuid": "^11.0.5",

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
public/forge/prime.worker.js


+ 1 - 1
src/core/config.js

@@ -3,7 +3,7 @@
  */
 
 const config = {
-  appName: '密码综合平台',
+  appName: '商密数据可信一体化平台',
   appLogo: '@/assets/logo.png',
   showViteLogo: true,
   logs: [],

+ 10 - 10
src/view/dashboard/index.vue

@@ -120,37 +120,37 @@ const getTruncatedContent = content => {
 
 const toolCards = ref([
   {
-    label: '创建考试',
+    label: '印章管理',
     icon: 'list',
-    name: 'examList',
+    name: 'picManager',
     color: '#ff9c6e',
     bg: 'rgba(255, 156, 110,.3)'
   },
   {
-    label: '新增学员',
+    label: '印模管理',
     icon: 'user',
-    name: 'studentManage',
+    name: 'sealManager',
     color: '#69c0ff',
     bg: 'rgba(105, 192, 255,.3)'
   },
   {
-    label: '新增课程',
+    label: '电子签章',
     icon: 'cellphone',
-    name: 'classesList',
+    name: 'signatureManager',
     color: '#b37feb',
     bg: 'rgba(179, 127, 235,.3)'
   },
   {
-    label: '新增题目',
+    label: '验证电子签章',
     icon: 'collection',
-    name: 'questionList',
+    name: 'signatureVerify',
     color: '#5cdbd3',
     bg: 'rgba(92, 219, 211,.3)'
   },
   {
-    label: '发布公告',
+    label: '文档转换服务',
     icon: 'bell',
-    name: 'publishNotice',
+    name: 'fIleConvert',
     color: '#C3CE2F',
     bg: 'rgba(195,206,47,.3)'
   }

+ 272 - 272
src/view/sysApplication/experiment/classicalCiphers/VigenereView.vue

@@ -1,219 +1,212 @@
 <template>
   <div class="vigenere-container">
-    <el-card
-      class="box-card"
-      shadow="never"
-    >
+    <el-card class="box-card" shadow="hover">
       <template #header><strong>维吉尼亚密码加解密与破解模拟</strong></template>
-      <el-tabs
-        v-model="activeTab"
-        type="border-card"
-        @tab-change="onTabChange"
-      >
-        <el-tab-pane
-          label="原理说明"
-          name="principle"
-        >
+      <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
+        <el-tab-pane label="原理说明" name="principle">
           <div class="principle-content">
             <h3>维吉尼亚密码工作原理</h3>
-            <p>维吉尼亚密码使用英文字母密钥进行重复位移。加密 Cᵢ = (Pᵢ + Kᵢ) mod 26,解密 Pᵢ = (Cᵢ - Kᵢ + 26) mod 26。密钥字母 A 对应位移 0,B 对应位移 1。</p>
+            <p>维吉尼亚密码(Vigenère Cipher)是一种多表代换密码。凯撒密码对所有字母使用同一个位移,维吉尼亚密码则由密钥词决定每个位置的位移,并循环使用密钥。因此,同一个明文字母出现在不同位置时,可能变成不同的密文字母;密钥只有一个字母时,它就退化为凯撒密码。</p>
+            <h4>1. 字母编码与循环密钥</h4>
+            <p>先将 A~Z 编码为 0~25。设明文第 i 个字母的数值为 Pᵢ,密钥长度为 L,密钥第 j 个字母的数值为 Kⱼ;下标从 0 开始。明文第 i 个位置使用 K[i mod L],即不断重复密钥,直到覆盖整个明文。</p>
+            <pre><code>字母:A  B  C  D  E  …  Z
+数值:0  1  2  3  4  … 25
+
+加密:Cᵢ = (Pᵢ + K[i mod L]) mod 26
+解密:Pᵢ = (Cᵢ - K[i mod L] + 26) mod 26</code></pre>
+            <p>模 26 表示超过 Z 后从 A 重新开始。例如 Z + B 对应 (25 + 1) mod 26 = 0,结果为 A。解密时加 26 是为了避免相减为负数;在 JavaScript 中,负数的 % 运算结果仍可能为负。</p>
+            <h4>2. 完整加解密示例</h4>
+            <p>明文为 ATTACK AT DAWN,密钥为 LEMON。移除空格后,明文有 12 个字母,密钥循环对齐如下:</p>
+            <pre><code>位置:  0  1  2  3  4  5  6  7  8  9 10 11
+明文:  A  T  T  A  C  K  A  T  D  A  W  N
+密钥:  L  E  M  O  N  L  E  M  O  N  L  E
+位移: 11  4 12 14 13 11  4 12 14 13 11  4
+密文:  L  X  F  O  P  V  E  F  R  N  H  R</code></pre>
+            <p>第 0 位:A(0) + L(11) → L(11);第 1 位:T(19) + E(4) → X(23);第 2 位:T(19) + M(12) → F(5)。解密第 2 位时,(5 - 12 + 26) mod 26 = 19,恢复为 T。最终密文为 LXFOPVEFRNHR,解密结果为 ATTACKATDAWN。</p>
+            <h4>3. 本页面的输入规则</h4>
             <p>本页面仅对 A–Z 字母进行运算,并统一转换为大写;空格、标点、数字、中文和其他字符均被过滤。密钥只允许英文字母。结果不能还原被过滤的字符。</p>
+            <p>界面要求密钥为 1~20 个英文字母,且明文或密文至少包含一个英文字母。过滤发生在密钥对齐之前,例如 A B 与 AB 使用完全相同的密钥位置。这里使用的是重复密钥版本,不是自动密钥密码。</p>
             <h3>统计破解原理</h3>
-            <p>首先尝试不同的密钥长度,将密文按位置分组,计算各组的重合指数(IC);再针对每组测试 26 种凯撒位移,用英语字母频率的卡方统计量选择较合理的位移。</p>
-            <el-alert
-              title="统计分析不能保证恢复正确密钥;短密文、非自然英语文本以及密钥长度的倍数都可能产生误判。"
-              type="warning"
-              :closable="false"
-              show-icon
-            />
-            <div class="flowchart-placeholder"><div
-              ref="mermaidContainer"
-              class="mermaid"
-            /></div>
+            <h4>1. 为什么可以拆成多个凯撒密码</h4>
+            <p>若密钥长度为 L,则下标为 j、j+L、j+2L……的字母都使用同一个密钥字母。因此,将密文按“下标 mod L”分成 L 组后,每组都只是一个固定移位的凯撒密码。例如 LEMON 长度为 5,第 0、5、10 位使用 L,第 1、6、11 位使用 E。破解可以分为“推测长度”和“逐组推测位移”两步。</p>
+            <h4>2. 用重合指数(IC)推测密钥长度</h4>
+            <pre><code>IC = Σ fⱼ(fⱼ - 1) / [N(N - 1)],j = 0…25
+N:组内字母总数;fⱼ:第 j 个字母在该组中的次数
+平均 IC = 各组 IC 之和 / 分组数 L</code></pre>
+            <p>IC 可以理解为从同一组中不放回地抽取两个字母,两者相同的概率。均匀随机字母的参考值约为 1/26 ≈ 0.0385,普通英语文本约为 0.065。固定的凯撒移位只改变字母名称,不改变次数分布,所以正确分组往往能保留英语文本的 IC 特征。</p>
+            <p>本页面尝试 1~min(20, ⌊密文字母数/5⌋) 的长度,按平均 IC 与 0.065 的距离从小到大保留前 5 个候选;距离相同则优先较短长度。这只是控制候选范围的启发式规则,并不意味着每组 5 个字母就足够可靠。正确长度的倍数也可能获得相似 IC。</p>
+            <h4>3. 用卡方统计量恢复密钥字母</h4>
+            <pre><code>对每组分别尝试解密位移 s = 0…25:
+  Oⱼ = 按位移 s 解密后,第 j 个字母的实际次数
+  Eⱼ = 该组长度 × 英语中第 j 个字母的参考概率
+  χ²(s) = Σ (Oⱼ - Eⱼ)² / Eⱼ
+取 χ² 最小的 s 作为该组候选位移;0→A,1→B,…,25→Z。</code></pre>
+            <p>英语字母出现概率并不均匀,例如 E 的参考频率约为 12.702%,T 约为 9.056%。卡方值衡量候选解密结果与该参考分布的偏离程度,越小表示频率越接近。将各组的候选位移转回字母并按组序拼接,就得到候选密钥,再解密整段密文并检查内容是否可读。</p>
+            <h4>4. 如何理解破解结果</h4>
+            <p>“平均卡方”是各组最小卡方值的算术平均,不是正确概率,也不适合作为不同密钥长度之间的严格置信度。页面先展示手选长度,再附加已有的候选长度,最多生成 5 行结果;每个长度只输出逐组最优位移组成的一把密钥,不会穷举全部 26ᴸ 种密钥,也未实现 Kasiski 重复片段分析或词典评分。</p>
+            <el-alert title="统计分析不能保证恢复正确密钥;短密文、非自然英语文本以及密钥长度的倍数都可能产生误判。" type="warning" :closable="false" show-icon />
+            <p>建议使用 100 个以上的英语字母,并结合候选明文的可读性手工选择长度、调整密钥。维吉尼亚密码适合学习古典密码与统计分析,不适合保护现代敏感数据。</p>
+            <div class="flowchart-placeholder"><div ref="mermaidContainer" class="mermaid"></div></div>
           </div>
         </el-tab-pane>
-        <el-tab-pane
-          label="算法实现"
-          name="implementation"
-        >
+        <el-tab-pane label="算法实现" name="implementation">
           <div class="code-content">
-            <h3>算法概要</h3>
-            <pre><code>加密:C[i] = (P[i] + K[i % L]) % 26
-解密:P[i] = (C[i] - K[i % L] + 26) % 26
-重合指数:IC = Σ f(c) × (f(c)-1) / (N × (N-1))
-卡方值:χ² = Σ (观察次数 - 预期次数)² / 预期次数
-对每个候选密钥长度 L:
-  1. 按下标模 L 分成 L 组
-  2. 计算平均 IC,按英语 IC 参考值排序
-  3. 每组尝试 26 种解密位移,取卡方值最小者
-  4. 拼接候选密钥并解密,供人工判断</code></pre>
-            <p>参考值 0.065 是英语自然文本的近似 IC,不是保证正确的判定阈值;卡方值也仅用于排序。</p>
+            <h3>1. 输入标准化与校验</h3>
+            <p>算法使用 JavaScript 字符编码进行运算,A 的编码为 65。文本先过滤再转大写;密钥必须先通过完整校验,不能把非法密钥中的字符静默删除。以下代码与本页实际计算逻辑对应,为便于阅读展开了部分表达式。</p>
+            <pre><code>const extractLetters = text =>
+  String(text ?? '').replace(/[^a-zA-Z]/g, '').toUpperCase()
+const validKey = key => /^[A-Za-z]{1,20}$/.test(key)
+
+// 界面在调用 encrypt / decrypt 前执行:
+if (!validKey(keyword)) throw new Error('密钥必须是 1–20 位英文字母')
+if (!extractLetters(text)) throw new Error('至少需要一个英文字母')</code></pre>
+            <h3>2. 加密与解密</h3>
+            <p>通过 i % k.length 获取循环密钥下标,将字母转换为 0~25,完成模运算后加回 65 转为字母。解密与加密的区别在于减去密钥位移。</p>
+            <pre><code>function encrypt(text, key) {
+  const p = extractLetters(text), k = extractLetters(key)
+  if (!k) throw new Error('请输入合法的英文字母密钥')
+  return Array.from(p, (ch, i) => {
+    const plain = ch.charCodeAt(0) - 65
+    const shift = k.charCodeAt(i % k.length) - 65
+    return String.fromCharCode(65 + (plain + shift) % 26)
+  }).join('')
+}
+
+function decrypt(text, key) {
+  const c = extractLetters(text), k = extractLetters(key)
+  if (!k) throw new Error('请输入合法的英文字母密钥')
+  return Array.from(c, (ch, i) => {
+    const cipher = ch.charCodeAt(0) - 65
+    const shift = k.charCodeAt(i % k.length) - 65
+    return String.fromCharCode(65 + (cipher - shift + 26) % 26)
+  }).join('')
+}
+
+encrypt('ATTACK AT DAWN', 'LEMON') // LXFOPVEFRNHR
+decrypt('LXFOPVEFRNHR', 'LEMON')   // ATTACKATDAWN</code></pre>
+            <h3>3. 分组与重合指数</h3>
+            <p>下列统计函数接收已经标准化的 A~Z 密文。每个字母按下标取模分配到对应组;不足两个字母的组无法抽取两个不同位置,IC 按 0 处理。</p>
+            <pre><code>function splitColumns(cipher, length) {
+  const groups = Array.from({ length }, () => '')
+  for (let i = 0; i &lt; cipher.length; i++) {
+    groups[i % length] += cipher[i]
+  }
+  return groups
+}
+
+function countLetters(text) {
+  const counts = Array(26).fill(0)
+  for (const ch of text) counts[ch.charCodeAt(0) - 65]++
+  return counts
+}
+
+function calcIC(text) {
+  const n = text.length
+  if (n &lt; 2) return 0
+  return countLetters(text).reduce((sum, f) => sum + f * (f - 1), 0)
+    / (n * (n - 1))
+}</code></pre>
+            <h3>4. 候选密钥长度排序</h3>
+            <p>对每个长度重新分组,计算各组 IC 的算术平均,再按与英语参考值的距离排序;返回的 length 和 ic 用于显示候选按钮。</p>
+            <pre><code>function guessLengths(cipher) {
+  const max = Math.min(20, Math.floor(cipher.length / 5))
+  if (max &lt; 1) return []
+  return Array.from({ length: max }, (_, i) => {
+    const length = i + 1
+    const groups = splitColumns(cipher, length)
+    const ic = groups.reduce((sum, g) => sum + calcIC(g), 0) / length
+    return { length, ic }
+  }).sort((a, b) =>
+    Math.abs(a.ic - 0.065) - Math.abs(b.ic - 0.065)
+    || a.length - b.length
+  ).slice(0, 5)
+}</code></pre>
+            <h3>5. 卡方评分与候选密钥恢复</h3>
+            <p>englishFreq 按 A~Z 的顺序保存英语字母参考频率,单位是百分比。直接统计“按某个位移解密后”的字母次数,无需为每次尝试构造完整明文。</p>
+            <pre><code>const englishFreq = [
+  8.167, 1.492, 2.782, 4.253, 12.702, 2.228, 2.015,
+  6.094, 6.966, 0.153, 0.772, 4.025, 2.406, 6.749,
+  7.507, 1.929, 0.095, 5.987, 6.327, 9.056, 2.758,
+  0.978, 2.360, 0.150, 1.974, 0.074
+]
+
+function chiSquare(text, shift) {
+  const n = text.length
+  if (!n) return Infinity
+  const counts = Array(26).fill(0)
+  for (const ch of text) {
+    counts[(ch.charCodeAt(0) - 65 - shift + 26) % 26]++
+  }
+  return counts.reduce((sum, observed, i) => {
+    const expected = n * englishFreq[i] / 100
+    return sum + (observed - expected) ** 2 / expected
+  }, 0)
+}
+
+function crackKeyword(cipher, length) {
+  const shifts = splitColumns(cipher, length).map(group => {
+    let best = 0, score = Infinity
+    for (let s = 0; s &lt; 26; s++) {
+      const candidate = chiSquare(group, s)
+      if (candidate &lt; score) { best = s; score = candidate }
+    }
+    return { shift: best, score }
+  })
+  return {
+    keyword: shifts.map(v => String.fromCharCode(65 + v.shift)).join(''),
+    score: shifts.reduce((sum, v) => sum + v.score, 0) / length
+  }
+}</code></pre>
+            <h3>6. 结果生成与验证</h3>
+            <ol>
+              <li>破解输入至少需要 20 个英文字母;手选长度必须是 1~20 的整数,且不能大于密文字母数。</li>
+              <li>合并手选长度与已有的候选长度,去掉与手选长度重复的项,最多处理 5 个长度。</li>
+              <li>对每个长度调用 crackKeyword,再用返回的密钥调用 decrypt,展示密钥、平均卡方和候选明文。</li>
+              <li>点击“使用候选”后,将结果回填到加解密表单;解密比对针对标准化后的明文,不包含原始空格与标点,也不能据此证明破解结果真实。</li>
+            </ol>
+            <p>若有效字母数为 N,单次加解密时间复杂度为 O(N)。固定长度 L 的密钥恢复约需 O(26N + 26²L) 运算;页面限制长度不超过 20,适合交互式教学。统计分数仅辅助分析,最终仍需检查语言内容和密钥合理性。</p>
           </div>
         </el-tab-pane>
-        <el-tab-pane
-          label="加解密与破解"
-          name="calculation"
-        >
-          <el-radio-group
-            v-model="mode"
-            class="mode-group"
-          >
+        <el-tab-pane label="加解密与破解" name="calculation">
+          <el-radio-group v-model="mode" class="mode-group">
             <el-radio value="encrypt">加解密模式</el-radio>
             <el-radio value="crack">破解模式</el-radio>
           </el-radio-group>
-          <el-form
-            v-if="mode === 'encrypt'"
-            :model="encryptForm"
-            label-width="100px"
-            class="calc-form"
-          >
-            <el-form-item label="明文"><el-input
-              v-model="encryptForm.plainText"
-              type="textarea"
-              :rows="3"
-              placeholder="示例:ATTACK AT DAWN"
-            /></el-form-item>
-            <el-form-item label="密钥词"><el-input
-              v-model="encryptForm.keyword"
-              maxlength="20"
-              placeholder="仅英文字母,如 LEMON"
-            /></el-form-item>
-            <el-form-item label="密文"><el-input
-              v-model="encryptForm.cipherText"
-              type="textarea"
-              :rows="3"
-              placeholder="加密结果,也可粘贴待解密密文"
-            /></el-form-item>
-            <el-form-item
-              label-width="0"
-              class="action-item"
-            ><div class="actions">
-              <el-button
-                type="primary"
-                :icon="Lock"
-                @click="handleEncrypt"
-              >加密计算</el-button>
-              <el-button
-                type="success"
-                :icon="Unlock"
-                @click="handleDecrypt"
-              >解密计算</el-button>
-              <el-button
-                :icon="Delete"
-                @click="clearEncrypt"
-              >清空</el-button>
+          <el-form v-if="mode === 'encrypt'" :model="encryptForm" label-width="100px" class="calc-form">
+            <el-form-item label="明文"><el-input v-model="encryptForm.plainText" type="textarea" :rows="3" placeholder="示例:ATTACK AT DAWN" /></el-form-item>
+            <el-form-item label="密钥词"><el-input v-model="encryptForm.keyword" maxlength="20" placeholder="仅英文字母,如 LEMON" /></el-form-item>
+            <el-form-item label="密文"><el-input v-model="encryptForm.cipherText" type="textarea" :rows="3" placeholder="加密结果,也可粘贴待解密密文" /></el-form-item>
+            <el-form-item label-width="0" class="action-item"><div class="actions">
+              <el-button type="primary" :icon="Lock" @click="handleEncrypt">加密计算</el-button>
+              <el-button type="success" :icon="Unlock" @click="handleDecrypt">解密计算</el-button>
+              <el-button :icon="Delete" @click="clearEncrypt">清空</el-button>
             </div></el-form-item>
-            <el-form-item
-              v-if="decryptedResult !== null"
-              label="解密结果"
-            ><el-input
-              :model-value="decryptedResult"
-              type="textarea"
-              :rows="3"
-              readonly
-            /></el-form-item>
-            <el-form-item v-if="decryptedResult !== null"><el-alert
-              :title="compareMessage"
-              :type="compareType"
-              :closable="false"
-              show-icon
-            /></el-form-item>
+            <el-form-item v-if="decryptedResult !== null" label="解密结果"><el-input :model-value="decryptedResult" type="textarea" :rows="3" readonly /></el-form-item>
+            <el-form-item v-if="decryptedResult !== null"><el-alert :title="compareMessage" :type="compareType" :closable="false" show-icon /></el-form-item>
           </el-form>
-          <el-form
-            v-else
-            :model="crackForm"
-            label-width="120px"
-            class="calc-form"
-          >
-            <el-form-item label="待破解密文"><el-input
-              v-model="crackForm.cipherText"
-              type="textarea"
-              :rows="4"
-              placeholder="建议至少 100 个英语字母,文本越长越容易分析"
-            /></el-form-item>
-            <el-form-item
-              label-width="0"
-              class="action-item"
-            ><div class="actions">
-              <el-button
-                type="warning"
-                :icon="Search"
-                @click="handleGuessLength"
-              >推测密钥长度</el-button>
-              <el-button
-                :icon="Delete"
-                @click="clearCrack"
-              >清空破解</el-button>
+          <el-form v-else :model="crackForm" label-width="120px" class="calc-form">
+            <el-form-item label="待破解密文"><el-input v-model="crackForm.cipherText" type="textarea" :rows="4" placeholder="建议至少 100 个英语字母,文本越长越容易分析" /></el-form-item>
+            <el-form-item label-width="0" class="action-item"><div class="actions">
+              <el-button type="warning" :icon="Search" @click="handleGuessLength">推测密钥长度</el-button>
+              <el-button :icon="Delete" @click="clearCrack">清空破解</el-button>
             </div></el-form-item>
-            <el-form-item
-              v-if="lengthOptions.length"
-              label="候选密钥长度"
-            >
+            <el-form-item v-if="lengthOptions.length" label="候选密钥长度">
               <div class="length-options">
-                <el-button
-                  v-for="item in lengthOptions"
-                  :key="item.length"
-                  :type="item.length === crackForm.targetLength ? 'primary' : 'default'"
-                  size="small"
-                  @click="selectLength(item.length)"
-                >
+                <el-button v-for="item in lengthOptions" :key="item.length" :type="item.length === crackForm.targetLength ? 'primary' : 'default'" size="small" @click="selectLength(item.length)">
                   {{ item.length }} (IC {{ item.ic.toFixed(4) }})
                 </el-button>
                 <div class="form-tips">按接近英语 IC 参考值排列;也可以手工选择其他长度。</div>
               </div>
             </el-form-item>
-            <el-form-item label="目标密钥长度"><div class="actions"><el-input-number
-              v-model="crackForm.targetLength"
-              :min="1"
-              :max="20"
-              :step="1"
-              step-strictly
-            /><el-button
-              type="primary"
-              :icon="MagicStick"
-              @click="handleAutoCrack"
-            >生成破解候选</el-button></div></el-form-item>
-            <div
-              v-if="crackResults.length"
-              class="crack-results"
-            >
+            <el-form-item label="目标密钥长度"><div class="actions"><el-input-number v-model="crackForm.targetLength" :min="1" :max="20" :step="1" step-strictly /><el-button type="primary" :icon="MagicStick" @click="handleAutoCrack">生成破解候选</el-button></div></el-form-item>
+            <div v-if="crackResults.length" class="crack-results">
               <h4>统计破解候选(并非保证正确)</h4>
-              <el-table
-                :data="crackResults"
-                border
-                stripe
-                style="width:100%"
-              >
-                <el-table-column
-                  prop="length"
-                  label="长度"
-                  width="74"
-                />
-                <el-table-column
-                  prop="keyword"
-                  label="候选密钥"
-                  width="160"
-                />
-                <el-table-column
-                  prop="score"
-                  label="平均卡方"
-                  width="106"
-                ><template #default="{ row }">{{ row.score.toFixed(2) }}</template></el-table-column>
-                <el-table-column
-                  prop="plainText"
-                  label="候选明文"
-                  min-width="230"
-                  show-overflow-tooltip
-                />
-                <el-table-column
-                  label="操作"
-                  width="105"
-                ><template #default="{ row }"><el-button
-                  size="small"
-                  type="primary"
-                  link
-                  @click="useCandidate(row)"
-                >使用候选</el-button></template></el-table-column>
+              <el-table :data="crackResults" border stripe style="width:100%">
+                <el-table-column prop="length" label="长度" width="74" />
+                <el-table-column prop="keyword" label="候选密钥" width="160" />
+                <el-table-column prop="score" label="平均卡方" width="106"><template #default="{ row }">{{ row.score.toFixed(2) }}</template></el-table-column>
+                <el-table-column prop="plainText" label="候选明文" min-width="230" show-overflow-tooltip />
+                <el-table-column label="操作" width="105"><template #default="{ row }"><el-button size="small" type="primary" link @click="useCandidate(row)">使用候选</el-button></template></el-table-column>
               </el-table>
             </div>
           </el-form>
@@ -239,130 +232,130 @@ const crackResults = ref([])
 const mermaidContainer = ref(null)
 const extractLetters = text => String(text ?? '').replace(/[^a-zA-Z]/g, '').toUpperCase()
 const validKey = key => /^[A-Za-z]{1,20}$/.test(key)
-const englishFreq = [8.167, 1.492, 2.782, 4.253, 12.702, 2.228, 2.015, 6.094, 6.966, 0.153, 0.772, 4.025, 2.406, 6.749, 7.507, 1.929, 0.095, 5.987, 6.327, 9.056, 2.758, 0.978, 2.360, 0.150, 1.974, 0.074]
-const encrypt = (text, key) => {
-  const p = extractLetters(text); const k = extractLetters(key)
+const englishFreq = [8.167,1.492,2.782,4.253,12.702,2.228,2.015,6.094,6.966,0.153,0.772,4.025,2.406,6.749,7.507,1.929,0.095,5.987,6.327,9.056,2.758,0.978,2.360,0.150,1.974,0.074]
+const encrypt = (text,key) => {
+  const p = extractLetters(text), k = extractLetters(key)
   if (!k) throw new Error('请输入合法的英文字母密钥')
-  return Array.from(p, (c, i) => String.fromCharCode(65 + (c.charCodeAt(0) - 65 + k.charCodeAt(i % k.length) - 65) % 26)).join('')
+  return Array.from(p,(c,i)=>String.fromCharCode(65+(c.charCodeAt(0)-65+k.charCodeAt(i%k.length)-65)%26)).join('')
 }
-const decrypt = (text, key) => {
-  const p = extractLetters(text); const k = extractLetters(key)
+const decrypt = (text,key) => {
+  const p = extractLetters(text), k = extractLetters(key)
   if (!k) throw new Error('请输入合法的英文字母密钥')
-  return Array.from(p, (c, i) => String.fromCharCode(65 + (c.charCodeAt(0) - k.charCodeAt(i % k.length) + 26) % 26)).join('')
+  return Array.from(p,(c,i)=>String.fromCharCode(65+(c.charCodeAt(0)-k.charCodeAt(i%k.length)+26)%26)).join('')
 }
 const countLetters = text => {
   const counts = Array(26).fill(0)
-  for (const c of text) counts[c.charCodeAt(0) - 65]++
+  for (const c of text) counts[c.charCodeAt(0)-65]++
   return counts
 }
 const calcIC = text => {
   const n = text.length
   if (n < 2) return 0
-  return countLetters(text).reduce((sum, c) => sum + c * (c - 1), 0) / (n * (n - 1))
+  return countLetters(text).reduce((sum,c)=>sum+c*(c-1),0)/(n*(n-1))
 }
-const splitColumns = (cipher, length) => {
-  const groups = Array.from({ length }, () => '')
-  for (let i = 0; i < cipher.length; i++) groups[i % length] += cipher[i]
+const splitColumns = (cipher,length) => {
+  const groups = Array.from({length},()=> '')
+  for (let i=0;i<cipher.length;i++) groups[i%length]+=cipher[i]
   return groups
 }
 const guessLengths = cipher => {
-  const max = Math.min(20, Math.floor(cipher.length / 5))
+  const max = Math.min(20,Math.floor(cipher.length/5))
   if (max < 1) return []
-  return Array.from({ length: max }, (_, i) => {
-    const length = i + 1
-    const groups = splitColumns(cipher, length)
-    const ic = groups.reduce((sum, g) => sum + calcIC(g), 0) / length
-    return { length, ic }
-  }).sort((a, b) => Math.abs(a.ic - 0.065) - Math.abs(b.ic - 0.065) || a.length - b.length).slice(0, 5)
+  return Array.from({length:max},(_,i)=>{
+    const length=i+1
+    const groups=splitColumns(cipher,length)
+    const ic=groups.reduce((sum,g)=>sum+calcIC(g),0)/length
+    return {length,ic}
+  }).sort((a,b)=>Math.abs(a.ic-0.065)-Math.abs(b.ic-0.065) || a.length-b.length).slice(0,5)
 }
-const chiSquare = (text, shift) => {
-  const n = text.length
+const chiSquare = (text,shift) => {
+  const n=text.length
   if (!n) return Infinity
-  const counts = Array(26).fill(0)
-  for (const ch of text) counts[(ch.charCodeAt(0) - 65 - shift + 26) % 26]++
-  return counts.reduce((sum, v, i) => {
-    const expected = n * englishFreq[i] / 100
-    return sum + (v - expected) ** 2 / expected
-  }, 0)
+  const counts=Array(26).fill(0)
+  for (const ch of text) counts[(ch.charCodeAt(0)-65-shift+26)%26]++
+  return counts.reduce((sum,v,i)=>{
+    const expected=n*englishFreq[i]/100
+    return sum+(v-expected)**2/expected
+  },0)
 }
-const crackKeyword = (cipher, length) => {
-  const groups = splitColumns(cipher, length)
-  const shifts = groups.map(group => {
-    let best = 0; let score = Infinity
-    for (let s = 0; s < 26; s++) {
-      const candidate = chiSquare(group, s)
-      if (candidate < score) { best = s; score = candidate }
+const crackKeyword = (cipher,length) => {
+  const groups=splitColumns(cipher,length)
+  const shifts=groups.map(group=>{
+    let best=0,score=Infinity
+    for(let s=0;s<26;s++) {
+      const candidate=chiSquare(group,s)
+      if(candidate<score){best=s;score=candidate}
     }
-    return { shift: best, score }
+    return {shift:best,score}
   })
-  return { keyword: shifts.map(v => String.fromCharCode(65 + v.shift)).join(''), score: shifts.reduce((s, v) => s + v.score, 0) / length }
+  return {keyword:shifts.map(v=>String.fromCharCode(65+v.shift)).join(''),score:shifts.reduce((s,v)=>s+v.score,0)/length}
 }
-const compareMessage = computed(() => {
-  const original = extractLetters(encryptForm.plainText)
+const compareMessage = computed(()=> {
+  const original=extractLetters(encryptForm.plainText)
   if (!original) return '已输出解密结果;未提供原明文,无法比较。'
   return original === decryptedResult.value ? '解密结果与标准化后的原始明文一致。' : '解密结果与标准化后的原始明文不一致,请核对密钥和密文。'
 })
-const compareType = computed(() => !extractLetters(encryptForm.plainText) ? 'info' : extractLetters(encryptForm.plainText) === decryptedResult.value ? 'success' : 'warning')
-watch(() => [encryptForm.plainText, encryptForm.cipherText, encryptForm.keyword], () => { decryptedResult.value = null })
-watch(() => crackForm.cipherText, () => { lengthOptions.value = []; crackResults.value = [] })
-watch(() => crackForm.targetLength, () => { crackResults.value = [] })
-function handleEncrypt() {
-  if (!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
-  if (!extractLetters(encryptForm.plainText)) return ElMessage.warning('明文至少包含一个英文字母')
-  encryptForm.cipherText = encrypt(encryptForm.plainText, encryptForm.keyword)
+const compareType = computed(()=> !extractLetters(encryptForm.plainText) ? 'info' : extractLetters(encryptForm.plainText)===decryptedResult.value ? 'success' : 'warning')
+watch(()=>[encryptForm.plainText,encryptForm.cipherText,encryptForm.keyword],()=>{decryptedResult.value=null})
+watch(()=>crackForm.cipherText,()=>{lengthOptions.value=[];crackResults.value=[]})
+watch(()=>crackForm.targetLength,()=>{crackResults.value=[]})
+function handleEncrypt(){
+  if(!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
+  if(!extractLetters(encryptForm.plainText)) return ElMessage.warning('明文至少包含一个英文字母')
+  encryptForm.cipherText=encrypt(encryptForm.plainText,encryptForm.keyword)
   ElMessage.success('加密完成')
 }
-function handleDecrypt() {
-  if (!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
-  if (!extractLetters(encryptForm.cipherText)) return ElMessage.warning('密文至少包含一个英文字母')
-  decryptedResult.value = decrypt(encryptForm.cipherText, encryptForm.keyword)
+function handleDecrypt(){
+  if(!validKey(encryptForm.keyword)) return ElMessage.warning('密钥必须是 1–20 位英文字母')
+  if(!extractLetters(encryptForm.cipherText)) return ElMessage.warning('密文至少包含一个英文字母')
+  decryptedResult.value=decrypt(encryptForm.cipherText,encryptForm.keyword)
   ElMessage.success('解密完成,请查看比较结果')
 }
-function clearEncrypt() { Object.assign(encryptForm, { plainText: '', cipherText: '', keyword: '' }); decryptedResult.value = null }
-function handleGuessLength() {
-  const cipher = extractLetters(crackForm.cipherText)
-  if (cipher.length < 20) return ElMessage.warning('至少需要 20 个英文字母,建议 100 个以上')
-  lengthOptions.value = guessLengths(cipher)
-  if (lengthOptions.value.length) crackForm.targetLength = lengthOptions.value[0].length
+function clearEncrypt(){Object.assign(encryptForm,{plainText:'',cipherText:'',keyword:''});decryptedResult.value=null}
+function handleGuessLength(){
+  const cipher=extractLetters(crackForm.cipherText)
+  if(cipher.length<20) return ElMessage.warning('至少需要 20 个英文字母,建议 100 个以上')
+  lengthOptions.value=guessLengths(cipher)
+  if(lengthOptions.value.length) crackForm.targetLength=lengthOptions.value[0].length
   ElMessage.info('已生成统计候选长度,建议结合明文可读性判断')
 }
-function selectLength(length) { crackForm.targetLength = length }
-function handleAutoCrack() {
-  const cipher = extractLetters(crackForm.cipherText)
-  const length = Number(crackForm.targetLength)
-  if (cipher.length < 20) return ElMessage.warning('破解至少需要 20 个英文字母')
-  if (!Number.isInteger(length) || length < 1 || length > 20 || length > cipher.length) return ElMessage.warning('密钥长度必须为 1–20 的整数,且不能大于密文字母数')
-  const lengths = [length, ...lengthOptions.value.map(o => o.length).filter(n => n !== length)].slice(0, 5)
-  crackResults.value = lengths.map(l => {
-    const candidate = crackKeyword(cipher, l)
-    return { length: l, keyword: candidate.keyword, score: candidate.score, plainText: decrypt(cipher, candidate.keyword) }
+function selectLength(length){crackForm.targetLength=length}
+function handleAutoCrack(){
+  const cipher=extractLetters(crackForm.cipherText)
+  const length=Number(crackForm.targetLength)
+  if(cipher.length<20) return ElMessage.warning('破解至少需要 20 个英文字母')
+  if(!Number.isInteger(length)||length<1||length>20||length>cipher.length) return ElMessage.warning('密钥长度必须为 1–20 的整数,且不能大于密文字母数')
+  const lengths=[length,...lengthOptions.value.map(o=>o.length).filter(n=>n!==length)].slice(0,5)
+  crackResults.value=lengths.map(l=>{
+    const candidate=crackKeyword(cipher,l)
+    return {length:l,keyword:candidate.keyword,score:candidate.score,plainText:decrypt(cipher,candidate.keyword)}
   })
   ElMessage.info('生成的是统计候选结果,正确性需人工判断')
 }
-function useCandidate(row) {
-  Object.assign(encryptForm, { plainText: row.plainText, cipherText: extractLetters(crackForm.cipherText), keyword: row.keyword })
-  mode.value = 'encrypt'
-  decryptedResult.value = null
+function useCandidate(row){
+  Object.assign(encryptForm,{plainText:row.plainText,cipherText:extractLetters(crackForm.cipherText),keyword:row.keyword})
+  mode.value='encrypt'
+  decryptedResult.value=null
   ElMessage.info('已回填候选,可继续修改或校验')
 }
-function clearCrack() { crackForm.cipherText = ''; crackForm.targetLength = 3; lengthOptions.value = []; crackResults.value = [] }
-const diagramCode = `flowchart TD
+function clearCrack(){crackForm.cipherText='';crackForm.targetLength=3;lengthOptions.value=[];crackResults.value=[]}
+const diagramCode=`flowchart TD
   A[输入明文和密钥] --> B[仅保留英文字母并大写]
   B --> C[密钥循环对齐明文]
   C --> D[逐字母计算 P 加 K 模 26]
   D --> E[输出大写密文]
   E --> F[解密时逐字母计算 C 减 K 模 26]`
-let chartRendered = false
-async function renderMermaid() {
-  if (chartRendered || !mermaidContainer.value) return
+let chartRendered=false
+async function renderMermaid(){
+  if(chartRendered || !mermaidContainer.value) return
   try {
-    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:'default'})
     await nextTick()
-    const { svg } = await mermaid.render('vigenere-flow-' + Math.random().toString(36).slice(2), diagramCode)
-    if (mermaidContainer.value) { mermaidContainer.value.innerHTML = svg; chartRendered = true }
-  } catch (error) { console.error('Mermaid render error', error); if (mermaidContainer.value) mermaidContainer.value.textContent = '流程图暂时无法显示,请参阅上方文字步骤。' }
+    const {svg}=await mermaid.render('vigenere-flow-'+Math.random().toString(36).slice(2),diagramCode)
+    if(mermaidContainer.value){mermaidContainer.value.innerHTML=svg;chartRendered=true}
+  } catch(error){console.error('Mermaid render error',error);if(mermaidContainer.value) mermaidContainer.value.textContent='流程图暂时无法显示,请参阅上方文字步骤。'}
 }
-async function onTabChange(tab) { if (tab === 'principle') { await nextTick(); await renderMermaid() } }
+async function onTabChange(tab){if(tab==='principle'){await nextTick();await renderMermaid()}}
 </script>
 
 <style scoped>
@@ -370,6 +363,13 @@ async function onTabChange(tab) { if (tab === 'principle') { await nextTick(); a
 .box-card{border-radius:8px}
 .principle-content,.code-content{line-height:1.8;color:var(--el-text-color-regular)}
 .principle-content h3,.code-content h3{color:var(--el-text-color-primary)}
+.principle-content h3,.code-content h3{margin:24px 0 12px}
+.principle-content h3:first-child,.code-content h3:first-child{margin-top:0}
+.principle-content h4{margin:20px 0 8px;color:var(--el-text-color-primary)}
+.principle-content p,.code-content p{margin:10px 0}
+.principle-content pre,.code-content pre{margin:12px 0 16px}
+.code-content ol{padding-left:24px;margin:12px 0}
+.code-content li{margin:6px 0}
 .flowchart-placeholder{margin-top:20px;padding:20px;background:var(--el-fill-color-light);border:1px dashed var(--el-border-color);border-radius:6px;overflow-x:auto}
 pre{background:#282c34;color:#e5e7eb;padding:16px;border-radius:6px;overflow-x:auto;line-height:1.6;font-size:13px}
 .calc-form{max-width:900px;margin:18px auto 0}
@@ -382,4 +382,4 @@ pre{background:#282c34;color:#e5e7eb;padding:16px;border-radius:6px;overflow-x:a
 .form-tips{font-size:12px;color:var(--el-text-color-secondary);width:100%}
 .crack-results{margin-top:25px;padding:15px;border:1px solid var(--el-border-color);border-radius:6px}
 @media(max-width:640px){.vigenere-container{padding:8px}.calc-form :deep(.el-form-item__label){width:100%!important;text-align:left}.calc-form :deep(.el-form-item__content){margin-left:0!important;width:100%}}
-</style>
+</style>

+ 244 - 0
src/view/sysApplication/experiment/hashAlgorithm/Md5View.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="md5-container">
+    <el-card class="box-card" shadow="hover">
+      <template #header><strong>MD5 杂凑算法原理实现与验证</strong></template>
+      <el-alert title="安全提示:MD5 已存在实用碰撞攻击,仅用于教学和兼容性校验;不应用于数字签名、身份认证或密码存储。" type="info" :closable="false" show-icon class="security-note" />
+      <el-tabs v-model="activeTab" type="border-card" @tab-change="onTabChange">
+        <el-tab-pane label="原理说明" name="principle">
+          <div class="principle-content">
+            <h3>MD5 算法工作原理</h3>
+            <p>MD5(Message-Digest Algorithm 5)是输出固定 128 位(16 字节)摘要的杂凑函数,通常表示为 32 位十六进制字符。它不需要密钥,也不能通过解密还原原文。MD5 已不具备可靠的抗碰撞性,相同摘要不能证明两份数据一定相同。</p>
+            <ol>
+              <li>按字节处理消息,在比特末尾填充 1 和若干个 0,使其长度模 512 等于 448。</li>
+              <li>追加原消息的 64 位小端比特长度,得到 512 位整数倍的消息块。</li>
+              <li>初始化四个 32 位状态字 A、B、C、D。</li>
+              <li>将每个 512 位消息块按小端序划分为 16 个 32 位字,执行 4 轮、每轮 16 步压缩,使用 F、G、H、I 函数、模加与循环左移。</li>
+              <li>将每块压缩结果与原状态按模 2^32 相加,最后按小端序拼接四个状态字,输出 128 位摘要。</li>
+            </ol>
+            <p><strong>关于盐值:</strong>本页面的“盐值”仅做简单的文本字节前后拼接,不是 HMAC,也不是安全密码哈希算法。</p>
+            <div ref="mermaidContainer" class="flowchart-placeholder" aria-label="MD5 哈希流程图"></div>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="算法实现" name="implementation">
+          <div class="code-content">
+            <h3>关键代码实现(CryptoJS)</h3>
+            <p>将输入按 UTF-8、Hex 或 Base64 解码为字节序列;盐值始终使用 UTF-8,然后计算 MD5:</p>
+            <pre><code>import CryptoJS from 'crypto-js'
+
+// messageBytes 是严格解析输入编码后的 WordArray
+const saltBytes = CryptoJS.enc.Utf8.parse(salt)
+const input = salt
+  ? (prefix
+      ? saltBytes.clone().concat(messageBytes.clone())
+      : messageBytes.clone().concat(saltBytes))
+  : messageBytes
+const digest = CryptoJS.MD5(input)
+const hex = digest.toString(CryptoJS.enc.Hex)</code></pre>
+            <p>验证时重新计算摘要,并与用户输入的期望摘要比较;这只检查摘要是否一致,并不验证消息来源身份。</p>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="计算与校验" name="calculation">
+          <el-form :model="form" label-width="110px" class="calc-form">
+            <el-form-item label="输入格式">
+              <el-radio-group v-model="form.inputFormat">
+                <el-radio value="string">UTF-8 文本</el-radio>
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="消息原文">
+              <el-input v-model="form.message" type="textarea" :rows="4" :placeholder="form.inputFormat === 'string' ? '输入文本;空字符串也是合法测试输入' : '输入严格编码的数据(可留空代表空字节串)'" />
+            </el-form-item>
+            <el-form-item label="盐值(可选)">
+              <el-input v-model="form.salt" placeholder="按 UTF-8 编码拼接,不属于安全密码存储" clearable />
+            </el-form-item>
+            <el-form-item label="拼接方式">
+              <el-select v-model="form.concatMethod" style="width: 100%" :disabled="!form.salt">
+                <el-option label="原文 + 盐值" value="suffix" />
+                <el-option label="盐值 + 原文" value="prefix" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="输出格式">
+              <el-radio-group v-model="form.outputFormat">
+                <el-radio value="hex">十六进制</el-radio>
+                <el-radio value="base64">Base64</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="计算结果">
+              <el-input v-model="form.result" readonly placeholder="点击“计算摘要”获取结果">
+                <template #append><el-button :icon="CopyDocument" aria-label="复制摘要" @click="copyResult" /></template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="待验证摘要">
+              <el-input v-model="form.expected" placeholder="输入或粘贴外部 MD5 摘要进行验证" clearable />
+            </el-form-item>
+            <el-form-item label-width="0" class="action-item">
+              <div class="button-group">
+                <el-button type="primary" :icon="Operation" @click="handleCalculate">计算摘要</el-button>
+                <el-button type="success" :icon="CircleCheck" @click="handleVerify">验证摘要</el-button>
+                <el-button :icon="RefreshRight" @click="handleClear">清空</el-button>
+              </div>
+            </el-form-item>
+            <el-alert v-if="verifyState" :title="verifyState.message" :type="verifyState.ok ? 'success' : 'error'" :closable="false" show-icon />
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue'
+import { ElMessage } from 'element-plus'
+import { CopyDocument, Operation, CircleCheck, RefreshRight } from '@element-plus/icons-vue'
+import CryptoJS from 'crypto-js'
+import mermaid from 'mermaid'
+
+const activeTab = ref('calculation')
+const form = reactive({
+  inputFormat: 'string', message: '', salt: '', concatMethod: 'suffix',
+  outputFormat: 'hex', result: '', expected: ''
+})
+const verifyState = ref(null)
+const mermaidContainer = ref(null)
+let diagramRendered = false
+let disposed = false
+
+// 空消息有效,但非空 Hex 必须是偶数位合法字符。
+function parseInput(data, format) {
+  if (format === 'string') return CryptoJS.enc.Utf8.parse(data)
+  if (format === 'hex') {
+    if (data && !/^(?:[0-9a-fA-F]{2})+$/.test(data)) {
+      throw new Error('Hex 格式错误:只能使用 0-9、a-f,且必须为偶数位')
+    }
+    return CryptoJS.enc.Hex.parse(data)
+  }
+  if (format === 'base64') {
+    const re = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
+    if (data && !re.test(data)) throw new Error('Base64 格式错误,请检查字符和填充符 =')
+    const parsed = CryptoJS.enc.Base64.parse(data)
+    // 严格检查规范编码,拒绝忽略了无效末尾比特的非规范 Base64。
+    if (parsed.toString(CryptoJS.enc.Base64) !== data) {
+      throw new Error('Base64 编码不规范')
+    }
+    return parsed
+  }
+  throw new Error('不支持的输入格式')
+}
+
+function calculateDigest() {
+  const messageBytes = parseInput(form.message, form.inputFormat)
+  const saltBytes = CryptoJS.enc.Utf8.parse(form.salt)
+  const input = !form.salt ? messageBytes : form.concatMethod === 'prefix'
+    ? saltBytes.clone().concat(messageBytes.clone())
+    : messageBytes.clone().concat(saltBytes)
+  const digest = CryptoJS.MD5(input)
+  return digest.toString(form.outputFormat === 'base64' ? CryptoJS.enc.Base64 : CryptoJS.enc.Hex)
+}
+
+function normalizeExpected(value) {
+  if (form.outputFormat === 'hex') {
+    if (!/^[0-9a-fA-F]{32}$/.test(value)) throw new Error('MD5 Hex 摘要必须恰好为 32 位十六进制字符')
+    return value.toLowerCase()
+  }
+  if (!/^[A-Za-z0-9+/]{22}==$/.test(value)) throw new Error('MD5 Base64 摘要必须为 24 位规范编码(以 == 结尾)')
+  const bytes = CryptoJS.enc.Base64.parse(value)
+  if (bytes.sigBytes !== 16 || bytes.toString(CryptoJS.enc.Base64) !== value) {
+    throw new Error('MD5 Base64 摘要内容或编码不合法')
+  }
+  return value
+}
+
+function handleCalculate() {
+  try {
+    const digest = calculateDigest()
+    form.result = digest
+    verifyState.value = null
+    ElMessage.success('摘要计算成功')
+  } catch (error) {
+    form.result = ''
+    verifyState.value = null
+    ElMessage.error(error.message || '计算失败')
+  }
+}
+function handleVerify() {
+  verifyState.value = null
+  try {
+    const expected = normalizeExpected(form.expected)
+    const actual = calculateDigest()
+    const ok = actual === expected
+    verifyState.value = { ok, message: ok ? '验证通过:当前输入的摘要与计算结果一致' : '验证失败:待验证摘要与计算结果不一致' }
+    if (ok) ElMessage.success('摘要匹配')
+    else ElMessage.error('摘要不匹配')
+  } catch (error) {
+    ElMessage.error(error.message || '验证失败')
+  }
+}
+function handleClear() {
+  form.message = ''
+  form.salt = ''
+  form.result = ''
+  form.expected = ''
+  verifyState.value = null
+}
+watch(() => [form.message, form.inputFormat, form.salt, form.concatMethod, form.outputFormat], () => {
+  form.result = ''
+  verifyState.value = null
+})
+watch(() => form.expected, () => { verifyState.value = null })
+
+async function copyResult() {
+  if (!form.result) return ElMessage.warning('请先计算摘要')
+  try {
+    await navigator.clipboard.writeText(form.result)
+    ElMessage.success('已复制到剪贴板')
+  } catch (error) {
+    ElMessage.warning('复制失败,请手动选择结果复制(可尝试 HTTPS 或 localhost)')
+  }
+}
+
+async function renderMermaid() {
+  if (diagramRendered || disposed) return
+  await nextTick()
+  if (!mermaidContainer.value || disposed) return
+  const chart = `flowchart TD
+    A[输入消息字节] --> B[填充到512位块]
+    B --> C[附加64位小端消息长度]
+    C --> D[初始化 A、B、C、D]
+    D --> E[按小端序划分16个消息字]
+    E --> F[执行4轮共64步压缩并累加状态]
+    F --> G{还有消息块?}
+    G -->|是| E
+    G -->|否| H[输出128位 MD5 摘要]`
+  try {
+    mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' })
+    const { svg } = await mermaid.render('md5-flow-' + Math.random().toString(36).slice(2), chart)
+    if (mermaidContainer.value && !disposed) {
+      mermaidContainer.value.innerHTML = svg
+      diagramRendered = true
+    }
+  } catch (error) {
+    console.error('MD5 流程图渲染失败', error)
+    if (mermaidContainer.value && !disposed) mermaidContainer.value.textContent = '流程图暂时无法显示,请参考上方文字说明。'
+  }
+}
+function onTabChange(name) { if (name === 'principle') renderMermaid() }
+onBeforeUnmount(() => { disposed = true })
+</script>
+
+<style scoped>
+.md5-container {  min-height: 100vh; }
+.box-card {border-radius: 8px; }
+.security-note { margin-bottom: 16px; }
+.principle-content, .code-content { line-height: 1.8; color: #606266; }
+.principle-content h3, .code-content h3 { color: #303133; }
+.flowchart-placeholder { margin-top: 20px; padding: 20px; min-height: 100px; overflow-x: auto; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 4px; }
+.flowchart-placeholder :deep(svg) { max-width: 100%; height: auto; }
+pre { background: #282c34; color: #e4e7ed; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 13px; line-height: 1.6; }
+code { font-family: Consolas, Monaco, monospace; }
+.calc-form { max-width: 800px; margin: 20px auto 0; }
+.button-group { display: flex; flex-wrap: wrap; gap: 12px; width: 100%; }
+.button-group .el-button { margin-left: 0; }
+.action-item { margin-top: 20px; }
+@media (max-width: 600px) { .md5-container { padding: 8px; } .calc-form :deep(.el-form-item) { display: block; } .calc-form :deep(.el-form-item__label) { display: block; width: auto !important; text-align: left; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
+</style>

+ 9 - 17
src/view/sysApplication/experiment/index.vue

@@ -44,14 +44,19 @@
     <div class="experiment-content">
 
       <div class="content-body">
-        <div v-if="selectedAlgorithm" class="experiment-component">
+        <div
+          v-if="selectedAlgorithm"
+          class="experiment-component"
+        >
           <component :is="selectedAlgorithm.component" />
         </div>
 
         <!-- 其他实验模块占位符 -->
-        <div v-else class="experiment-placeholder">
-          <el-empty description="请从左侧选择实验模块">
-          </el-empty>
+        <div
+          v-else
+          class="experiment-placeholder"
+        >
+          <el-empty description="请从左侧选择实验模块" />
         </div>
       </div>
     </div>
@@ -61,19 +66,6 @@
 <script setup>
 import { ref, computed } from 'vue'
 import { Lock, Document } from '@element-plus/icons-vue'
-import AesView from './views/AesView.vue'
-import CaesarView from './views/CaesarView.vue'
-import DesView from './views/DesView.vue'
-import HmacView from './views/HmacView.vue'
-import RailView from './views/RailView.vue'
-import RsaEnc from './views/RsaEnc.vue'
-import RsaSign from './views/RsaSign.vue'
-import Sha1View from './views/Sha1View.vue'
-import Sha256View from './views/Sha256View.vue'
-import Sm2EncView from './views/Sm2EncView.vue'
-import Sm2SignView from './views/Sm2SignView.vue'
-import Sm4View from './views/Sm4View.vue'
-import VigenereView from './views/VigenereView.vue'
 
 const currentActive = ref('aes')
 

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

@@ -474,10 +474,12 @@ const submitCreate = async() => {
   createLoading.value = true
   console.log('生成印章', createForm.value)
   try {
-    await createSeal(createForm.value)
-    ElMessage.success('生成印章成功')
-    addSealDialogVisible.value = false
-    await getTableData()
+    const res = await createSeal(createForm.value)
+    if (res.code === 100000) {
+      ElMessage.success('生成印章成功')
+      addSealDialogVisible.value = false
+      await getTableData()
+    }
   } catch (err) {
     ElMessage.error(err.message || '生成印章失败')
   } finally {

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor