|
@@ -0,0 +1,292 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="des3-container">
|
|
|
|
|
+ <el-card class="box-card" shadow="never">
|
|
|
|
|
+ <template #header><strong>DES / 3DES 加解密教学实验</strong></template>
|
|
|
|
|
+ <el-tabs v-model="activeTab" type="border-card">
|
|
|
|
|
+ <el-tab-pane label="原理说明" name="principle">
|
|
|
|
|
+ <section class="article">
|
|
|
|
|
+ <h3>DES 与 3DES 工作原理</h3>
|
|
|
|
|
+ <p>DES 是分组长度为 64 位、有效密钥长度为 56 位的对称分组密码;密钥通常按包含校验位的 8 字节形式输入。3DES(TDEA)以 EDE(加密-解密-加密)结构重复使用 DES,本实验使用 24 字节的三密钥形式。</p>
|
|
|
|
|
+ <p>ECB 对每个分组独立处理,会泄露重复明文的模式。CBC 先将明文分组与前一个密文分组(首块使用 IV)异或再加密,IV 为 8 字节;每次实际 CBC 加密应使用新的不可预测 IV。</p>
|
|
|
|
|
+ <p>PKCS#7 会补足 8 字节的整数倍,恰好整块时也会额外补一块;NoPadding 则要求明文字节数为 8 的整数倍。文本采用 UTF-8 编码,Hex 表示的是原始字节。</p>
|
|
|
|
|
+ <el-alert type="warning" show-icon :closable="false" title="仅用于教学" description="DES 已不安全,3DES 也是不再推荐用于新系统的旧算法。CBC/ECB + PKCS#7 不提供消息完整性认证;实际业务建议使用 AES-GCM 等认证加密方案。" />
|
|
|
|
|
+ <div class="diagram-wrap"><div ref="mermaidContainer" class="mermaid">{{ diagramCode }}</div></div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ <el-tab-pane label="算法实现" name="implementation">
|
|
|
|
|
+ <section class="article">
|
|
|
|
|
+ <h3>基于 CryptoJS 的核心调用</h3>
|
|
|
|
|
+ <p>下方是关键 API 示意。实际页面另外实施了严格的 Hex、Base64、分组长度和 PKCS#7 填充校验。</p>
|
|
|
|
|
+ <pre><code>import CryptoJS from 'crypto-js';
|
|
|
|
|
+
|
|
|
|
|
+const algorithm = CryptoJS.TripleDES; // DES 则使用 CryptoJS.DES
|
|
|
|
|
+const key = CryptoJS.enc.Hex.parse(keyHex); // 3DES: 24 字节
|
|
|
|
|
+const iv = CryptoJS.enc.Hex.parse(ivHex); // CBC: 8 字节
|
|
|
|
|
+const plaintext = CryptoJS.enc.Utf8.parse('Hello');
|
|
|
|
|
+
|
|
|
|
|
+const encrypted = algorithm.encrypt(plaintext, key, {
|
|
|
|
|
+ iv,
|
|
|
|
|
+ mode: CryptoJS.mode.CBC,
|
|
|
|
|
+ padding: CryptoJS.pad.Pkcs7
|
|
|
|
|
+});
|
|
|
|
|
+const ciphertextHex = encrypted.ciphertext.toString(CryptoJS.enc.Hex);</code></pre>
|
|
|
|
|
+ <p>注意:CryptoJS 的解密接口本身不能证明密文来源可信;错误的密钥或篡改后的密文即使通过了填充检查,也可能产生无意义数据。</p>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </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.algorithm" @change="onAlgorithmChange">
|
|
|
|
|
+ <el-radio value="des">DES(8 字节密钥)</el-radio>
|
|
|
|
|
+ <el-radio value="3des">3DES(24 字节密钥)</el-radio>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="工作模式">
|
|
|
|
|
+ <el-radio-group v-model="form.mode" @change="clearResults">
|
|
|
|
|
+ <el-radio value="cbc">CBC</el-radio>
|
|
|
|
|
+ <el-radio value="ecb">ECB</el-radio>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="填充方式">
|
|
|
|
|
+ <el-radio-group v-model="form.padding" @change="clearResults">
|
|
|
|
|
+ <el-radio value="pkcs7">PKCS#7</el-radio>
|
|
|
|
|
+ <el-radio value="none">NoPadding</el-radio>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="密钥(Hex)" required>
|
|
|
|
|
+ <div class="field-row">
|
|
|
|
|
+ <el-input v-model="form.key" :placeholder="`输入 ${keyBytes * 2} 位十六进制字符`" clearable @input="clearResults" />
|
|
|
|
|
+ <el-button :icon="Refresh" @click="generateKey">生成密钥</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="hint">{{ form.algorithm === 'des' ? 'DES 使用 8 字节(含校验位,安全强度仅 56 位)' : '本实验使用三密钥 3DES(共 24 字节)' }};请输入 {{ keyBytes * 2 }} 位 Hex。</div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item v-if="form.mode === 'cbc'" label="IV(Hex)" required>
|
|
|
|
|
+ <div class="field-row">
|
|
|
|
|
+ <el-input v-model="form.iv" placeholder="16 位十六进制字符(8 字节)" clearable @input="clearResults" />
|
|
|
|
|
+ <el-button :icon="Refresh" @click="generateIv">生成 IV</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-checkbox v-model="form.autoIv" @change="clearResults">加密时自动生成新 IV(推荐)</el-checkbox>
|
|
|
|
|
+ <div class="hint">解密时会使用当前显示的 IV,请与密文一起保存;固定 IV 仅用于复现教学测试向量。</div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="明文输入格式">
|
|
|
|
|
+ <el-radio-group v-model="form.inputFormat" @change="clearResults">
|
|
|
|
|
+ <el-radio value="utf8">UTF-8 文本</el-radio>
|
|
|
|
|
+ <el-radio value="hex">Hex 字节</el-radio>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="明文" required>
|
|
|
|
|
+ <el-input v-model="form.plaintext" type="textarea" :rows="3" :placeholder="form.inputFormat === 'utf8' ? '输入普通文本(支持中文)' : '输入偶数位 Hex,例如 0123456789ABCDEF'" @input="clearResults" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="密文格式">
|
|
|
|
|
+ <el-radio-group v-model="form.cipherFormat" @change="onCipherFormatChange">
|
|
|
|
|
+ <el-radio value="hex">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.ciphertext" type="textarea" :rows="3" placeholder="加密结果,或粘贴待解密的密文" @input="onCiphertextInput" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="解密结果">
|
|
|
|
|
+ <el-input v-model="decryptedText" type="textarea" :rows="3" readonly placeholder="点击解密后显示结果,不会修改原明文" />
|
|
|
|
|
+ <div class="hint">{{ form.inputFormat === 'utf8' ? '按照 UTF-8 解码结果;无法解码时将提示改用 Hex。' : '按照 Hex 输出原始解密字节。' }}</div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label-width="0">
|
|
|
|
|
+ <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="handleClear">清空数据</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-alert v-if="statusMessage" class="result-alert" :type="statusType" :closable="false" show-icon :title="statusMessage" />
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ </el-tabs>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
|
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
|
|
+import { Refresh, Lock, Unlock, Delete } from '@element-plus/icons-vue';
|
|
|
|
|
+import CryptoJS from 'crypto-js';
|
|
|
|
|
+import mermaid from 'mermaid';
|
|
|
|
|
+
|
|
|
|
|
+const activeTab = ref('calculation');
|
|
|
|
|
+const mermaidContainer = ref(null);
|
|
|
|
|
+const diagramCode = `flowchart TD
|
|
|
|
|
+ A[输入字节、密钥、模式] --> B{CBC 模式?}
|
|
|
|
|
+ B -- 是 --> C[使用 8 字节 IV]
|
|
|
|
|
+ B -- 否 --> D[不使用 IV]
|
|
|
|
|
+ C --> E[PKCS7 填充或不填充]
|
|
|
|
|
+ D --> E
|
|
|
|
|
+ E --> F[DES 或 3DES 分组加密]
|
|
|
|
|
+ F --> G[输出 Hex 或 Base64 密文]
|
|
|
|
|
+ G --> H[按相同参数解密]
|
|
|
|
|
+`;
|
|
|
|
|
+let diagramRendered = false;
|
|
|
|
|
+let disposed = false;
|
|
|
|
|
+async function renderDiagram() {
|
|
|
|
|
+ if (diagramRendered || activeTab.value !== 'principle') return;
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ const node = mermaidContainer.value;
|
|
|
|
|
+ if (!node || disposed) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default' });
|
|
|
|
|
+ const uniqueId = `mermaid-des3-${Math.random().toString(36).slice(2)}`;
|
|
|
|
|
+ const { svg, bindFunctions } = await mermaid.render(uniqueId, diagramCode);
|
|
|
|
|
+ if (disposed || mermaidContainer.value !== node) return;
|
|
|
|
|
+ node.innerHTML = svg;
|
|
|
|
|
+ bindFunctions?.(node);
|
|
|
|
|
+ diagramRendered = true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('流程图绘制失败:', error);
|
|
|
|
|
+ if (!disposed && mermaidContainer.value === node) node.textContent = '流程图渲染失败,请检查 Mermaid 版本。';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+watch(activeTab, (tab) => { if (tab === 'principle') void renderDiagram(); }, { flush: 'post' });
|
|
|
|
|
+onBeforeUnmount(() => { disposed = true; });
|
|
|
|
|
+
|
|
|
|
|
+const form = reactive({
|
|
|
|
|
+ algorithm: '3des', mode: 'cbc', padding: 'pkcs7', key: '', iv: '', autoIv: true,
|
|
|
|
|
+ inputFormat: 'utf8', plaintext: 'Hello DES / 3DES!', cipherFormat: 'hex', ciphertext: ''
|
|
|
|
|
+});
|
|
|
|
|
+const keyBytes = computed(() => form.algorithm === 'des' ? 8 : 24);
|
|
|
|
|
+const decryptedText = ref('');
|
|
|
|
|
+const statusMessage = ref('');
|
|
|
|
|
+const statusType = ref('info');
|
|
|
|
|
+let lastCipherBytes = null;
|
|
|
|
|
+function clearResults() { decryptedText.value = ''; statusMessage.value = ''; lastCipherBytes = null; }
|
|
|
|
|
+function messageError(error) {
|
|
|
|
|
+ const text = error instanceof Error ? error.message : String(error);
|
|
|
|
|
+ statusMessage.value = text;
|
|
|
|
|
+ statusType.value = 'error';
|
|
|
|
|
+ ElMessage.error(text);
|
|
|
|
|
+}
|
|
|
|
|
+function requireHex(value, byteLength, name) {
|
|
|
|
|
+ if (typeof value !== 'string' || !/^(?:[0-9a-fA-F]{2})+$/.test(value) || (byteLength !== null && value.length !== byteLength * 2)) {
|
|
|
|
|
+ throw new Error(`${name}必须是${byteLength === null ? '非空、偶数位的' : `${byteLength * 2} 位`}合法 Hex 字符`);
|
|
|
|
|
+ }
|
|
|
|
|
+ return CryptoJS.enc.Hex.parse(value);
|
|
|
|
|
+}
|
|
|
|
|
+function wordArrayToBytes(wordArray) {
|
|
|
|
|
+ const bytes = new Uint8Array(wordArray.sigBytes);
|
|
|
|
|
+ for (let i = 0; i < bytes.length; i++) bytes[i] = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
|
|
|
|
+ return bytes;
|
|
|
|
|
+}
|
|
|
|
|
+function bytesToWordArray(bytes) {
|
|
|
|
|
+ const words = [];
|
|
|
|
|
+ for (let i = 0; i < bytes.length; i++) words[i >>> 2] = (words[i >>> 2] || 0) | (bytes[i] << (24 - (i % 4) * 8));
|
|
|
|
|
+ return CryptoJS.lib.WordArray.create(words, bytes.length);
|
|
|
|
|
+}
|
|
|
|
|
+function parseBase64(value) {
|
|
|
|
|
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value) || !value) {
|
|
|
|
|
+ throw new Error('密文不是合法的标准 Base64');
|
|
|
|
|
+ }
|
|
|
|
|
+ const parsed = CryptoJS.enc.Base64.parse(value);
|
|
|
|
|
+ if (parsed.toString(CryptoJS.enc.Base64) !== value) throw new Error('Base64 编码不规范');
|
|
|
|
|
+ return parsed;
|
|
|
|
|
+}
|
|
|
|
|
+function readCiphertext() {
|
|
|
|
|
+ const str = form.ciphertext.trim();
|
|
|
|
|
+ const wa = form.cipherFormat === 'hex' ? requireHex(str, null, '密文') : parseBase64(str);
|
|
|
|
|
+ if (wa.sigBytes === 0 || wa.sigBytes % 8 !== 0) throw new Error('DES/3DES 密文字节数必须为非零的 8 的整数倍');
|
|
|
|
|
+ return wa;
|
|
|
|
|
+}
|
|
|
|
|
+function parameters() {
|
|
|
|
|
+ const key = requireHex(form.key.trim(), keyBytes.value, '密钥');
|
|
|
|
|
+ const iv = form.mode === 'cbc' ? requireHex(form.iv.trim(), 8, 'IV') : undefined;
|
|
|
|
|
+ return { key, iv, algorithm: form.algorithm === 'des' ? CryptoJS.DES : CryptoJS.TripleDES,
|
|
|
|
|
+ options: { mode: form.mode === 'cbc' ? CryptoJS.mode.CBC : CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding, ...(iv ? { iv } : {}) } };
|
|
|
|
|
+}
|
|
|
|
|
+function plaintextBytes() {
|
|
|
|
|
+ if (!form.plaintext) throw new Error('请输入明文');
|
|
|
|
|
+ return form.inputFormat === 'hex' ? requireHex(form.plaintext.trim(), null, '明文') : CryptoJS.enc.Utf8.parse(form.plaintext);
|
|
|
|
|
+}
|
|
|
|
|
+function addPadding(bytes) {
|
|
|
|
|
+ const padLen = 8 - (bytes.length % 8);
|
|
|
|
|
+ const padded = new Uint8Array(bytes.length + padLen);
|
|
|
|
|
+ padded.set(bytes);
|
|
|
|
|
+ padded.fill(padLen, bytes.length);
|
|
|
|
|
+ return padded;
|
|
|
|
|
+}
|
|
|
|
|
+function stripPadding(bytes) {
|
|
|
|
|
+ if (!bytes.length || bytes.length % 8 !== 0) throw new Error('解密结果长度不正确');
|
|
|
|
|
+ const padLen = bytes[bytes.length - 1];
|
|
|
|
|
+ if (padLen < 1 || padLen > 8) throw new Error('PKCS#7 填充无效:密钥、IV、模式或密文可能不匹配');
|
|
|
|
|
+ for (let i = bytes.length - padLen; i < bytes.length; i++) {
|
|
|
|
|
+ if (bytes[i] !== padLen) throw new Error('PKCS#7 填充无效:密钥、IV、模式或密文可能不匹配');
|
|
|
|
|
+ }
|
|
|
|
|
+ return bytes.slice(0, bytes.length - padLen);
|
|
|
|
|
+}
|
|
|
|
|
+function setCiphertext(wa) {
|
|
|
|
|
+ lastCipherBytes = wa.clone();
|
|
|
|
|
+ form.ciphertext = wa.toString(form.cipherFormat === 'hex' ? CryptoJS.enc.Hex : CryptoJS.enc.Base64);
|
|
|
|
|
+}
|
|
|
|
|
+function handleEncrypt() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const bytes = wordArrayToBytes(plaintextBytes());
|
|
|
|
|
+ if (form.padding === 'none' && bytes.length % 8 !== 0) throw new Error('NoPadding 模式要求明文字节数是 8 的整数倍');
|
|
|
|
|
+ if (form.mode === 'cbc' && form.autoIv) form.iv = CryptoJS.lib.WordArray.random(8).toString(CryptoJS.enc.Hex);
|
|
|
|
|
+ const { key, algorithm, options } = parameters();
|
|
|
|
|
+ const input = bytesToWordArray(form.padding === 'pkcs7' ? addPadding(bytes) : bytes);
|
|
|
|
|
+ const encrypted = algorithm.encrypt(input, key, options);
|
|
|
|
|
+ setCiphertext(encrypted.ciphertext);
|
|
|
|
|
+ decryptedText.value = '';
|
|
|
|
|
+ statusMessage.value = `加密完成,共 ${encrypted.ciphertext.sigBytes} 字节密文;请保存密钥${form.mode === 'cbc' ? '及本次 IV' : ''}。`;
|
|
|
|
|
+ statusType.value = 'success';
|
|
|
|
|
+ ElMessage.success('加密完成');
|
|
|
|
|
+ } catch (error) { clearResults(); messageError(error); }
|
|
|
|
|
+}
|
|
|
|
|
+function handleDecrypt() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ciphertext = readCiphertext();
|
|
|
|
|
+ const { key, algorithm, options } = parameters();
|
|
|
|
|
+ const raw = algorithm.decrypt(CryptoJS.lib.CipherParams.create({ ciphertext }), key, options);
|
|
|
|
|
+ const resultBytes = form.padding === 'pkcs7' ? stripPadding(wordArrayToBytes(raw)) : wordArrayToBytes(raw);
|
|
|
|
|
+ if (form.inputFormat === 'hex') {
|
|
|
|
|
+ decryptedText.value = bytesToWordArray(resultBytes).toString(CryptoJS.enc.Hex);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ try { decryptedText.value = new TextDecoder('utf-8', { fatal: true }).decode(resultBytes); }
|
|
|
|
|
+ catch { throw new Error('解密结果不是合法 UTF-8,请切换“明文输入格式”为 Hex 查看原始字节,或核对密钥与参数'); }
|
|
|
|
|
+ }
|
|
|
|
|
+ statusMessage.value = '解密计算完成;填充校验通过不等于密文经过真实性认证。';
|
|
|
|
|
+ statusType.value = 'success';
|
|
|
|
|
+ ElMessage.success('解密完成');
|
|
|
|
|
+ } catch (error) { decryptedText.value = ''; messageError(error); }
|
|
|
|
|
+}
|
|
|
|
|
+function generateKey() { form.key = CryptoJS.lib.WordArray.random(keyBytes.value).toString(CryptoJS.enc.Hex); clearResults(); }
|
|
|
|
|
+function generateIv() { form.iv = CryptoJS.lib.WordArray.random(8).toString(CryptoJS.enc.Hex); clearResults(); }
|
|
|
|
|
+function onAlgorithmChange() { form.key = ''; clearResults(); }
|
|
|
|
|
+function onCiphertextInput() { decryptedText.value = ''; statusMessage.value = ''; lastCipherBytes = null; }
|
|
|
|
|
+function onCipherFormatChange() {
|
|
|
|
|
+ if (lastCipherBytes) form.ciphertext = lastCipherBytes.toString(form.cipherFormat === 'hex' ? CryptoJS.enc.Hex : CryptoJS.enc.Base64);
|
|
|
|
|
+ else { form.ciphertext = ''; }
|
|
|
|
|
+ decryptedText.value = '';
|
|
|
|
|
+ statusMessage.value = '';
|
|
|
|
|
+}
|
|
|
|
|
+function handleClear() {
|
|
|
|
|
+ Object.assign(form, { algorithm: '3des', mode: 'cbc', padding: 'pkcs7', key: '', iv: '', autoIv: true,
|
|
|
|
|
+ inputFormat: 'utf8', plaintext: '', cipherFormat: 'hex', ciphertext: '' });
|
|
|
|
|
+ clearResults();
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.des3-container { min-height: 100vh; }
|
|
|
|
|
+.box-card { border-radius: 8px; }
|
|
|
|
|
+.article { color: #606266; line-height: 1.8; }
|
|
|
|
|
+.article h3 { color: #303133; }
|
|
|
|
|
+.article pre { padding: 18px; background: #282c34; color: #e0e4eb; border-radius: 6px; overflow-x: auto; line-height: 1.6; }
|
|
|
|
|
+.diagram-wrap { padding: 24px; margin-top: 20px; background: #fafafa; border: 1px dashed #dcdfe6; border-radius: 6px; overflow-x: auto; }
|
|
|
|
|
+.calc-form { max-width: 850px; margin: 22px auto 0; }
|
|
|
|
|
+.field-row { width: 100%; display: flex; flex-wrap: wrap; gap: 10px; }
|
|
|
|
|
+.field-row :deep(.el-input) { flex: 1 1 280px; min-width: 0; }
|
|
|
|
|
+.field-row :deep(.el-button) { margin-left: 0; }
|
|
|
|
|
+.hint { width: 100%; margin-top: 5px; color: #909399; font-size: 12px; line-height: 1.6; }
|
|
|
|
|
+.actions { display: flex; width: 100%; gap: 12px; flex-wrap: wrap; }
|
|
|
|
|
+.actions :deep(.el-button + .el-button) { margin-left: 0; }
|
|
|
|
|
+.result-alert { margin-top: 18px; }
|
|
|
|
|
+.mermaid { overflow-x: auto; }
|
|
|
|
|
+@media (max-width: 600px) { .des3-container { padding: 8px; } .calc-form :deep(.el-form-item__label) { width: 100% !important; justify-content: flex-start; } .calc-form :deep(.el-form-item__content) { margin-left: 0 !important; } }
|
|
|
|
|
+</style>
|