| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- <template>
- <div class="vigenere-container">
- <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">
- <div class="principle-content">
- <h3>维吉尼亚密码工作原理</h3>
- <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>
- <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">
- <div class="code-content">
- <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 < 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 < 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 < 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 < 26; s++) {
- const candidate = chiSquare(group, s)
- if (candidate < 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-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>
- </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>
- <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="候选密钥长度">
- <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)">
- {{ 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">
- <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>
- </div>
- </el-form>
- </el-tab-pane>
- </el-tabs>
- </el-card>
- </div>
- </template>
- <script setup>
- import { computed, nextTick, reactive, ref, watch } from 'vue'
- import { ElMessage } from 'element-plus'
- import { Lock, Unlock, Delete, Search, MagicStick } from '@element-plus/icons-vue'
- import mermaid from 'mermaid'
- const activeTab = ref('calculation')
- const mode = ref('encrypt')
- const encryptForm = reactive({ plainText: '', cipherText: '', keyword: '' })
- const crackForm = reactive({ cipherText: '', targetLength: 3 })
- const decryptedResult = ref(null)
- const lengthOptions = ref([])
- 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), 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('')
- }
- 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('')
- }
- const countLetters = text => {
- const counts = Array(26).fill(0)
- 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))
- }
- 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))
- 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)
- }
- 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 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 {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)
- 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)
- 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)
- 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
- 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)}
- })
- ElMessage.info('生成的是统计候选结果,正确性需人工判断')
- }
- 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
- 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
- try {
- 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='流程图暂时无法显示,请参阅上方文字步骤。'}
- }
- async function onTabChange(tab){if(tab==='principle'){await nextTick();await renderMermaid()}}
- </script>
- <style scoped>
- .vigenere-container{min-height:100vh}
- .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}
- .mode-group{margin-bottom:12px}
- .action-item :deep(.el-form-item__content){margin-left:0!important}
- .actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;width:100%}
- .actions :deep(.el-button){margin-left:0}
- .length-options{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
- .length-options :deep(.el-button){margin-left:0}
- .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>
|