- 将原始 div 结构替换为 el-main 和 el-card 组件 - 移除自定义容器样式,采用 Element Plus 默认样式 - 调整文本域边框样式从 border 改为 outline - 禁用文本域调整大小功能 - 优化按钮状态样式过渡效果 - 移除页面底部说明区域 - 更新侧边栏菜单项路由配置为实际路径
381 lines
7.9 KiB
Vue
381 lines
7.9 KiB
Vue
<template>
|
||
<el-main>
|
||
<el-card>
|
||
|
||
<h1>🔑 字节对象 → Hex 密钥</h1>
|
||
<p class="sub">
|
||
将键为数字索引的 JSON 对象转换为 32 位十六进制字符串(适用于 AES‑128 密钥)
|
||
</p>
|
||
|
||
<label for="jsonInput">输入 JSON 对象</label>
|
||
<textarea
|
||
id="jsonInput"
|
||
v-model="inputText"
|
||
placeholder='例如:{"0":14,"1":3,"2":121,...}'
|
||
@keydown.ctrl.enter="performConversion"
|
||
@keydown.meta.enter="performConversion"
|
||
></textarea>
|
||
|
||
<div class="actions">
|
||
<button @click="performConversion">🔄 转换</button>
|
||
<button class="secondary" :disabled="!copyEnabled" @click="copyResult">
|
||
📋 复制 Hex
|
||
</button>
|
||
<span class="copy-feedback" :class="{ show: copyFeedbackVisible }"
|
||
>已复制!</span
|
||
>
|
||
</div>
|
||
|
||
<div class="result-area">
|
||
<div class="result-label">
|
||
<span>转换结果</span>
|
||
</div>
|
||
<div
|
||
class="result-box"
|
||
:class="resultClass"
|
||
v-text="resultText"
|
||
></div>
|
||
</div>
|
||
|
||
<div class="example-hint">
|
||
💡 点击
|
||
<button class="link-btn" @click="loadExample">加载示例</button>
|
||
快速体验
|
||
</div>
|
||
</el-card>
|
||
</el-main>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import {ref, onMounted} from 'vue'
|
||
|
||
// 示例数据
|
||
const EXAMPLE_OBJ: Record<string, number> = {
|
||
'0': 14,
|
||
'1': 3,
|
||
'2': 121,
|
||
'3': 117,
|
||
'4': 7,
|
||
'5': 245,
|
||
'6': 22,
|
||
'7': 34,
|
||
'8': 136,
|
||
'9': 163,
|
||
'10': 201,
|
||
'11': 114,
|
||
'12': 195,
|
||
'13': 230,
|
||
'14': 109,
|
||
'15': 204,
|
||
}
|
||
|
||
// 响应式数据
|
||
const inputText = ref<string>('')
|
||
const resultText = ref<string>('等待转换…')
|
||
const resultClass = ref<string>('') // 用于 success / error
|
||
const copyFeedbackVisible = ref<boolean>(false)
|
||
const copyEnabled = ref<boolean>(false)
|
||
|
||
// 核心转换函数
|
||
function convertToHex(obj: Record<string, unknown>): string {
|
||
// 获取所有键并转为数字排序
|
||
const keys = Object.keys(obj)
|
||
.map((k) => Number(k))
|
||
.sort((a, b) => a - b)
|
||
|
||
// 检查是否有非数字键
|
||
if (keys.some((k) => isNaN(k))) {
|
||
throw new Error('对象包含非数字键,请确保键为 "0", "1", … 格式')
|
||
}
|
||
|
||
// 检查长度是否为16
|
||
if (keys.length !== 16) {
|
||
throw new Error(`期望 16 个字节,实际获得 ${keys.length} 个`)
|
||
}
|
||
|
||
// 按顺序取值并转为两位十六进制
|
||
const hex = keys
|
||
.map((key) => {
|
||
const val = obj[key]
|
||
if (typeof val !== 'number' || isNaN(val)) {
|
||
throw new Error(`键 "${key}" 对应的值不是有效数字 (${val})`)
|
||
}
|
||
if (val < 0 || val > 255) {
|
||
throw new Error(`键 "${key}" 的值 ${val} 超出字节范围 (0-255)`)
|
||
}
|
||
return val.toString(16).padStart(2, '0')
|
||
})
|
||
.join('')
|
||
|
||
return hex
|
||
}
|
||
|
||
// 执行转换
|
||
function performConversion(): void {
|
||
const raw = inputText.value.trim()
|
||
if (!raw) {
|
||
resultText.value = '⚠️ 请输入 JSON 数据'
|
||
resultClass.value = 'error'
|
||
copyEnabled.value = false
|
||
copyFeedbackVisible.value = false
|
||
return
|
||
}
|
||
|
||
try {
|
||
const obj = JSON.parse(raw)
|
||
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
||
throw new Error('输入必须是 JSON 对象(不是数组或原始类型)')
|
||
}
|
||
const hex = convertToHex(obj)
|
||
resultText.value = hex
|
||
resultClass.value = 'success'
|
||
copyEnabled.value = true
|
||
copyFeedbackVisible.value = false
|
||
} catch (err: any) {
|
||
resultText.value = `❌ 转换失败:${err.message}`
|
||
resultClass.value = 'error'
|
||
copyEnabled.value = false
|
||
copyFeedbackVisible.value = false
|
||
}
|
||
}
|
||
|
||
// 加载示例
|
||
function loadExample(): void {
|
||
inputText.value = JSON.stringify(EXAMPLE_OBJ, null, 2)
|
||
performConversion() // 自动转换
|
||
}
|
||
|
||
// 复制结果
|
||
function copyResult(): void {
|
||
const text = resultText.value
|
||
if (!text || resultClass.value === 'error') return
|
||
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard
|
||
.writeText(text)
|
||
.then(() => {
|
||
copyFeedbackVisible.value = true
|
||
setTimeout(() => (copyFeedbackVisible.value = false), 2000)
|
||
})
|
||
.catch(() => {
|
||
fallbackCopy(text)
|
||
})
|
||
} else {
|
||
fallbackCopy(text)
|
||
}
|
||
}
|
||
|
||
function fallbackCopy(text: string): void {
|
||
const textarea = document.createElement('textarea')
|
||
textarea.value = text
|
||
document.body.appendChild(textarea)
|
||
textarea.select()
|
||
try {
|
||
document.execCommand('copy')
|
||
copyFeedbackVisible.value = true
|
||
setTimeout(() => (copyFeedbackVisible.value = false), 2000)
|
||
} catch (_) {
|
||
alert('复制失败,请手动复制')
|
||
}
|
||
document.body.removeChild(textarea)
|
||
}
|
||
|
||
// 组件挂载时加载示例
|
||
onMounted(() => {
|
||
loadExample()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// 变量(可根据需要调整)
|
||
$primary: #3b82f6;
|
||
$primary-hover: #2563eb;
|
||
$bg: #f6f8fa;
|
||
$card-bg: #ffffff;
|
||
$text-dark: #1f2937;
|
||
$text-gray: #6b7280;
|
||
$border: #d1d5db;
|
||
$radius: 16px;
|
||
|
||
:deep(.el-card__body) {
|
||
//height: calc(100vh - 211px + 60px);
|
||
height: calc(100vh - 141px);
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 22px;
|
||
font-weight: 600;
|
||
margin-top: 0;
|
||
margin-bottom: 8px;
|
||
color: $text-dark;
|
||
}
|
||
|
||
.sub {
|
||
color: $text-gray;
|
||
font-size: 14px;
|
||
margin-top: -4px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
label {
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
color: #374151;
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
height: 200px;
|
||
padding: 12px 14px;
|
||
outline: 1px solid $border;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-family: 'Menlo', 'Cascadia Code', 'Consolas', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
resize: none;
|
||
transition: border 0.2s, background 0.2s;
|
||
background: #fafbfc;
|
||
|
||
&:focus {
|
||
border-color: $primary;
|
||
outline: none;
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||
background: white;
|
||
}
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin: 18px 0 14px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
button {
|
||
background: $primary;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 24px;
|
||
border-radius: 8px;
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: background 0.15s, transform 0.1s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
|
||
&:hover {
|
||
background: $primary-hover;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.96);
|
||
}
|
||
|
||
&.secondary {
|
||
background: #e5e7eb;
|
||
color: $text-dark;
|
||
|
||
&:hover {
|
||
background: #d1d5db;
|
||
}
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
|
||
&:hover {
|
||
background: #e5e7eb;
|
||
}
|
||
}
|
||
|
||
&.link-btn {
|
||
background: none;
|
||
border: none;
|
||
color: $primary;
|
||
font-weight: 500;
|
||
padding: 0;
|
||
font-size: 13px;
|
||
text-decoration: underline;
|
||
display: inline;
|
||
|
||
&:hover {
|
||
color: $primary-hover;
|
||
background: none;
|
||
transform: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.copy-feedback {
|
||
font-size: 13px;
|
||
color: #10b981;
|
||
margin-left: 12px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
|
||
&.show {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.result-area {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.result-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 4px;
|
||
|
||
span {
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
}
|
||
|
||
.result-box {
|
||
background: #f3f4f6;
|
||
border-radius: 10px;
|
||
padding: 14px 16px;
|
||
font-family: 'Menlo', 'Cascadia Code', 'Consolas', monospace;
|
||
font-size: 14px;
|
||
word-break: break-all;
|
||
min-height: 48px;
|
||
border: 1px solid #e5e7eb;
|
||
color: $text-dark;
|
||
transition: background 0.2s, border-color 0.2s;
|
||
|
||
&.error {
|
||
background: #fee2e2;
|
||
border-color: #fca5a5;
|
||
color: #991b1b;
|
||
}
|
||
|
||
&.success {
|
||
background: #ecfdf5;
|
||
border-color: #6ee7b7;
|
||
}
|
||
}
|
||
|
||
.example-hint {
|
||
font-size: 13px;
|
||
color: $text-gray;
|
||
margin-top: 6px;
|
||
}
|
||
</style>
|