Files
ToolBox/src/views/EncoderDecoder.vue
T
renjue f1502ed08c
CI / docker (push) Successful in 11m42s
初始化 ToolBox 开发者工具箱。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 11:15:38 +08:00

1173 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="tool-page">
<!-- 浮层提示 -->
<Transition name="toast">
<div v-if="toastMessage" class="toast-notification" :class="toastType">
<div class="toast-content">
<i v-if="toastType === 'error'" class="fas fa-circle-exclamation"></i>
<i v-else class="fas fa-circle-info"></i>
<span>{{ toastMessage }}</span>
</div>
<button @click="closeToast" class="toast-close-btn" title="关闭">
<i class="fas fa-xmark"></i>
</button>
</div>
</Transition>
<div class="main-container">
<!-- 左侧侧栏历史记录 -->
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
<div class="sidebar-header">
<h3>历史记录</h3>
<button @click="toggleSidebar" class="close-btn">×</button>
</div>
<div class="sidebar-content">
<div v-if="historyList.length === 0" class="empty-history">
暂无历史记录
</div>
<div
v-for="(item, index) in historyList"
:key="index"
class="history-item"
@click="loadHistory(item)"
>
<div class="history-header">
<span class="history-type">{{ item.type === 'encode' ? '编码' : '解码' }}</span>
<span class="history-encoding">{{
item.encodingType === 'base64' ? 'Base64' :
item.encodingType === 'url' ? 'URL' :
item.encodingType === 'unicode' ? 'Unicode' :
item.encodingType === 'zlib' ? 'Zlib' :
item.encodingType
}}</span>
<span class="history-time">{{ formatTime(item.time) }}</span>
</div>
<div class="history-preview">{{ truncateText(item.input, 50) }}</div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div class="content-wrapper" :class="{ 'sidebar-pushed': sidebarOpen }">
<!-- 左侧输入区域 -->
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
<div class="panel-toolbar">
<div class="view-tabs">
<button class="view-tab active">输入</button>
</div>
<div class="toolbar-actions">
<div class="encoding-type-selector">
<button
@click="encodingType = 'base64'"
:class="['type-btn', { active: encodingType === 'base64' }]"
title="Base64编码"
>
Base64
</button>
<button
@click="encodingType = 'url'"
:class="['type-btn', { active: encodingType === 'url' }]"
title="URL编码"
>
URL
</button>
<button
@click="encodingType = 'unicode'"
:class="['type-btn', { active: encodingType === 'unicode' }]"
title="Unicode编码"
>
Unicode
</button>
<button
@click="encodingType = 'zlib'"
:class="['type-btn', { active: encodingType === 'zlib' }]"
title="Zlib 压缩/解压"
>
Zlib
</button>
</div>
<button @click="copyInputToClipboard" class="toolbar-icon-btn" title="复制">
<i class="far fa-copy"></i>
</button>
<button @click="pasteFromClipboard" class="toolbar-icon-btn" title="粘贴">
<i class="far fa-paste"></i>
</button>
<button @click="encode" class="toolbar-icon-btn" title="编码">
<i class="fa-solid fa-code"></i>
</button>
<button @click="decode" class="toolbar-icon-btn" title="解码">
<svg viewBox="150 -50 1100 1250" xmlns="http://www.w3.org/2000/svg" width="17" height="17"><path d="M285.352637 0.003641h111.956538v114.687184h-111.956538v282.621991a110.682235 110.682235 0 0 1-33.313896 81.282266 110.682235 110.682235 0 0 1-81.282266 33.313896 110.682235 110.682235 0 0 1 81.282266 33.313897 110.682235 110.682235 0 0 1 33.313896 81.282266v282.621991h111.956538v114.687184h-111.956538a188.050574 188.050574 0 0 1-80.007964-40.049493 93.570179 93.570179 0 0 1-34.67922-74.637691v-226.643722a110.682235 110.682235 0 0 0-33.313896-81.282267 110.682235 110.682235 0 0 0-81.282267-33.313896H0v-111.956537h55.978269a110.682235 110.682235 0 0 0 81.282266-33.313897 110.682235 110.682235 0 0 0 33.313896-81.282266V114.690825A113.776969 113.776969 0 0 1 285.261616 0.003641z m794.61835 0a113.776969 113.776969 0 0 1 114.687184 114.687184v226.643722a113.776969 113.776969 0 0 0 114.687185 114.687184H1365.323624v111.956538h-55.978268a113.776969 113.776969 0 0 0-114.687185 114.687184v226.643722a113.776969 113.776969 0 0 1-114.687184 114.687184h-111.956537V909.309175h111.956537V626.687184a113.776969 113.776969 0 0 1 114.687184-114.687184 113.776969 113.776969 0 0 1-114.687184-114.687184V114.690825h-111.956537V0.003641h111.956537zM682.661812 682.665453a54.612945 54.612945 0 0 1 55.978269 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 54.612945 54.612945 0 0 1-80.007965 0 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z m-226.643721 0a54.612945 54.612945 0 0 1 55.978268 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 52.246384 52.246384 0 0 1-40.049493 17.294099 59.164024 59.164024 0 0 1-58.708916-58.708916 52.246384 52.246384 0 0 1 17.294099-40.049493 58.799937 58.799937 0 0 1 41.505839-15.837754z m453.287443 0a58.799937 58.799937 0 0 1 41.323795 16.019797 52.246384 52.246384 0 0 1 17.294099 40.049493 59.164024 59.164024 0 0 1-58.708916 58.708916 52.246384 52.246384 0 0 1-40.049493-17.294099 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z" fill="#666666" p-id="26339"></path></svg>
</button>
<button @click="clearAll" class="toolbar-icon-btn" title="清空">
<i class="far fa-trash-can"></i>
</button>
</div>
</div>
<div ref="inputContainerRef" class="editor-container">
<div class="editor-body">
<div class="line-numbers">
<div v-for="n in inputLineCount" :key="n" class="line-number">{{ n }}</div>
</div>
<textarea
ref="inputEditorRef"
v-model="inputText"
@input="updateInputLineCount"
placeholder="请输入要编码或解码的文本"
class="text-editor"
></textarea>
</div>
</div>
<div class="sidebar-toggle">
<button @click="toggleSidebar" class="toggle-btn">
{{ sidebarOpen ? '◀' : '▶' }}
</button>
</div>
</div>
<!-- 可拖拽分割线 -->
<div
class="splitter"
@mousedown="startResize"
></div>
<!-- 右侧输出区域 -->
<div class="right-panel" :style="{ width: rightPanelWidth + '%' }">
<div class="panel-toolbar">
<div class="view-tabs">
<button class="view-tab active">输出</button>
</div>
<div class="toolbar-actions">
<button @click="copyOutputToClipboard" class="toolbar-icon-btn" title="复制输出">
<i class="far fa-copy"></i>
</button>
</div>
</div>
<div ref="outputContainerRef" class="editor-container">
<div class="editor-body">
<div class="line-numbers">
<div v-for="n in outputLineCount" :key="n" class="line-number">{{ n }}</div>
</div>
<textarea
ref="outputEditorRef"
v-model="outputText"
readonly
class="text-editor output-editor"
placeholder="编码或解码结果将显示在这里"
></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { zlibSync, decompressSync } from 'fflate'
import { useLineNumberEditor } from '../composables/useLineNumberEditor'
const inputText = ref('')
const outputText = ref('')
const encodingType = ref('base64') // 'base64'、'url'、'unicode' 或 'zlib'
const leftPanelWidth = ref(50)
const rightPanelWidth = ref(50)
const isResizing = ref(false)
const {
containerRef: inputContainerRef,
editorRef: inputEditorRef,
lineCount: inputLineCount,
updateLineCount: updateInputLineCount,
initEditor: initInputEditor,
} = useLineNumberEditor(() => inputText.value)
const {
containerRef: outputContainerRef,
editorRef: outputEditorRef,
lineCount: outputLineCount,
updateLineCount: updateOutputLineCount,
initEditor: initOutputEditor,
} = useLineNumberEditor(() => outputText.value)
const sidebarOpen = ref(false)
// 历史记录
const historyList = ref([])
const STORAGE_KEY = 'encoder-decoder-history'
const MAX_HISTORY = 50
// 提示消息
const toastMessage = ref('')
const toastType = ref('error') // 'error' 或 'info'
let toastTimer = null
// 显示提示
const showToast = (message, type = 'error', duration = 3000) => {
toastMessage.value = message
toastType.value = type
if (toastTimer) {
clearTimeout(toastTimer)
}
toastTimer = setTimeout(() => {
toastMessage.value = ''
}, duration)
}
// 关闭提示
const closeToast = () => {
if (toastTimer) {
clearTimeout(toastTimer)
toastTimer = null
}
toastMessage.value = ''
}
// 监听输出变化,同步行号
watch(() => outputText.value, () => {
updateOutputLineCount()
})
// Zlib: Uint8Array -> Base64(用于显示/复制)
const bytesToBase64 = (bytes) => {
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
// Zlib: Base64 -> Uint8Array(自动去除空白字符)
const base64ToBytes = (str) => {
const clean = str.replace(/\s/g, '')
const binary = atob(clean)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes
}
// Unicode 编码
const encodeUnicode = (text) => {
let result = ''
let i = 0
while (i < text.length) {
const codePoint = text.codePointAt(i)
if (codePoint > 0xFFFF) {
// 超出 BMP 的字符,使用代理对表示
const high = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800
const low = ((codePoint - 0x10000) % 0x400) + 0xDC00
result += '\\u' + high.toString(16).toUpperCase().padStart(4, '0')
result += '\\u' + low.toString(16).toUpperCase().padStart(4, '0')
i += 2 // 代理对占用两个字符位置
} else {
// BMP 字符
result += '\\u' + codePoint.toString(16).toUpperCase().padStart(4, '0')
i++
}
}
return result
}
// Unicode 解码
const decodeUnicode = (text) => {
try {
let result = ''
let i = 0
while (i < text.length) {
// 匹配 \uXXXX 格式
if (text[i] === '\\' && i + 1 < text.length && text[i + 1] === 'u' && i + 5 < text.length) {
const hex = text.substring(i + 2, i + 6)
if (/^[0-9a-fA-F]{4}$/.test(hex)) {
const code1 = parseInt(hex, 16)
// 检查是否是高代理(surrogate high
if (code1 >= 0xD800 && code1 <= 0xDBFF && i + 11 < text.length) {
// 检查下一个是否是低代理
if (text[i + 6] === '\\' && text[i + 7] === 'u') {
const hex2 = text.substring(i + 8, i + 12)
if (/^[0-9a-fA-F]{4}$/.test(hex2)) {
const code2 = parseInt(hex2, 16)
// 检查是否是低代理(surrogate low
if (code2 >= 0xDC00 && code2 <= 0xDFFF) {
// 组合代理对
const codePoint = 0x10000 + ((code1 - 0xD800) << 10) + (code2 - 0xDC00)
result += String.fromCodePoint(codePoint)
i += 12
continue
}
}
}
}
// 普通字符或单独的代理
result += String.fromCharCode(code1)
i += 6
continue
}
}
// 匹配 \UXXXXXXXX 格式(8位十六进制)
if (text[i] === '\\' && i + 1 < text.length && text[i + 1] === 'U' && i + 9 < text.length) {
const hex = text.substring(i + 2, i + 10)
if (/^[0-9a-fA-F]{8}$/.test(hex)) {
const code = parseInt(hex, 16)
if (code > 0x10FFFF) {
throw new Error('无效的 Unicode 码点:超出范围')
}
result += String.fromCodePoint(code)
i += 10
continue
}
}
// 普通字符
result += text[i]
i++
}
return result
} catch (e) {
throw new Error('Unicode 解码失败:' + e.message)
}
}
const encode = () => {
if (!inputText.value.trim()) {
showToast('请输入要编码的文本')
return
}
try {
let result = ''
if (encodingType.value === 'base64') {
result = btoa(unescape(encodeURIComponent(inputText.value)))
} else if (encodingType.value === 'url') {
result = encodeURIComponent(inputText.value)
} else if (encodingType.value === 'unicode') {
result = encodeUnicode(inputText.value)
} else if (encodingType.value === 'zlib') {
const utf8 = new TextEncoder().encode(inputText.value)
// 使用 zlib 格式,与 Java Deflater/Inflater 默认格式兼容
const compressed = zlibSync(utf8)
result = bytesToBase64(compressed)
}
outputText.value = result
// 保存到历史记录
saveToHistory({
type: 'encode',
encodingType: encodingType.value,
input: inputText.value,
output: result
})
showToast('编码成功', 'info', 2000)
} catch (e) {
showToast('编码失败:' + e.message)
outputText.value = ''
}
}
const decode = () => {
if (!inputText.value.trim()) {
showToast('请输入要解码的字符串')
return
}
try {
let result = ''
if (encodingType.value === 'base64') {
result = decodeURIComponent(escape(atob(inputText.value)))
} else if (encodingType.value === 'url') {
result = decodeURIComponent(inputText.value)
} else if (encodingType.value === 'unicode') {
result = decodeUnicode(inputText.value)
} else if (encodingType.value === 'zlib') {
const compressed = base64ToBytes(inputText.value.trim())
// decompressSync 自动识别 zlib / raw deflate / gzip,可解 Java Deflater 输出
const decompressed = decompressSync(compressed)
result = new TextDecoder().decode(decompressed)
}
outputText.value = result
// 保存到历史记录
saveToHistory({
type: 'decode',
encodingType: encodingType.value,
input: inputText.value,
output: result
})
showToast('解码成功', 'info', 2000)
} catch (e) {
const typeName = encodingType.value === 'base64' ? 'Base64' :
encodingType.value === 'url' ? 'URL' :
encodingType.value === 'unicode' ? 'Unicode' :
encodingType.value === 'zlib' ? 'Zlib' :
encodingType.value
showToast(`解码失败:请检查输入是否为有效的${typeName}编码字符串`)
outputText.value = ''
}
}
const clearAll = () => {
inputText.value = ''
outputText.value = ''
updateInputLineCount()
updateOutputLineCount()
showToast('已清空', 'info', 2000)
}
// 复制输入到剪贴板
const copyInputToClipboard = async () => {
if (!inputText.value.trim()) {
showToast('输入内容为空,无法复制')
return
}
try {
await navigator.clipboard.writeText(inputText.value)
showToast('已复制输入到剪贴板', 'info', 2000)
} catch (e) {
showToast('复制失败:' + e.message)
}
}
// 复制输出到剪贴板
const copyOutputToClipboard = async () => {
if (!outputText.value.trim()) {
showToast('输出内容为空,无法复制')
return
}
try {
await navigator.clipboard.writeText(outputText.value)
showToast('已复制输出到剪贴板', 'info', 2000)
} catch (e) {
showToast('复制失败:' + e.message)
}
}
// 从剪贴板粘贴
const pasteFromClipboard = async () => {
if (navigator.clipboard && navigator.clipboard.readText) {
try {
const text = await navigator.clipboard.readText()
if (text.trim()) {
inputText.value = text
updateInputLineCount()
} else {
showToast('剪贴板内容为空')
}
} catch (e) {
if (inputEditorRef.value) {
inputEditorRef.value.focus()
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
} else {
showToast('无法访问编辑器,请手动粘贴内容')
}
}
} else {
if (inputEditorRef.value) {
inputEditorRef.value.focus()
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
}
}
}
// 拖拽调整宽度
const startResize = (e) => {
isResizing.value = true
document.addEventListener('mousemove', handleResize)
document.addEventListener('mouseup', stopResize)
e.preventDefault()
}
const handleResize = (e) => {
if (!isResizing.value) return
const container = document.querySelector('.content-wrapper')
if (!container) return
const containerWidth = container.offsetWidth
const mouseX = e.clientX - container.getBoundingClientRect().left
const percentage = (mouseX / containerWidth) * 100
// 限制在20%到80%之间
const leftPercent = Math.max(20, Math.min(80, percentage))
const rightPercent = 100 - leftPercent
leftPanelWidth.value = leftPercent
rightPanelWidth.value = rightPercent
}
const stopResize = () => {
isResizing.value = false
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
}
// 保存到历史记录
const saveToHistory = (item) => {
const historyItem = {
...item,
time: Date.now()
}
// 从localStorage读取现有历史
let history = []
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
history = JSON.parse(stored)
}
} catch (e) {
// 读取历史记录失败,忽略错误
}
// 避免重复保存相同的记录
const lastHistory = history[0]
if (lastHistory &&
lastHistory.type === historyItem.type &&
lastHistory.encodingType === historyItem.encodingType &&
lastHistory.input === historyItem.input &&
lastHistory.output === historyItem.output) {
return
}
// 添加到开头
history.unshift(historyItem)
// 限制最多50条
if (history.length > MAX_HISTORY) {
history = history.slice(0, MAX_HISTORY)
}
// 保存到localStorage
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
loadHistoryList()
} catch (e) {
// 保存历史记录失败,忽略错误
}
}
// 加载历史记录列表
const loadHistoryList = () => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
historyList.value = JSON.parse(stored)
}
} catch (e) {
// 加载历史记录失败,重置为空数组
historyList.value = []
}
}
// 加载历史记录
const loadHistory = (item) => {
inputText.value = item.input
outputText.value = item.output
encodingType.value = item.encodingType
updateInputLineCount()
updateOutputLineCount()
}
// 切换侧栏
const toggleSidebar = () => {
sidebarOpen.value = !sidebarOpen.value
}
// 格式化时间
const formatTime = (timestamp) => {
const date = new Date(timestamp)
const now = new Date()
const diff = now - date
if (diff < 60000) return '刚刚'
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 截断文本
const truncateText = (text, maxLength) => {
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + '...'
}
onMounted(() => {
initInputEditor()
initOutputEditor()
loadHistoryList()
})
onUnmounted(() => {
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
if (toastTimer) {
clearTimeout(toastTimer)
}
})
</script>
<style scoped>
/* 浮层提示样式 */
.toast-notification {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
min-width: 300px;
max-width: 90%;
padding: 0.75rem 1rem;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
font-size: 0.875rem;
}
.toast-notification.error {
background: #fff5f5;
color: #c33;
border: 1px solid #ffcccc;
}
.toast-notification.info {
background: #f0f9ff;
color: #0369a1;
border: 1px solid #bae6fd;
}
.toast-content {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.toast-content svg,
.toast-content i {
flex-shrink: 0;
}
.toast-content span {
flex: 1;
word-break: break-word;
}
.toast-close-btn {
flex-shrink: 0;
margin-left: 0.75rem;
padding: 0.25rem;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: inherit;
opacity: 0.6;
transition: all 0.2s;
width: 20px;
height: 20px;
}
.toast-close-btn:hover {
opacity: 1;
background: rgba(0, 0, 0, 0.08);
}
.toast-notification.error .toast-close-btn:hover {
background: rgba(204, 51, 51, 0.15);
}
.toast-notification.info .toast-close-btn:hover {
background: rgba(3, 105, 161, 0.15);
}
.toast-close-btn:active {
opacity: 0.8;
}
.toast-close-btn svg,
.toast-close-btn i {
display: block;
font-size: 14px;
}
/* 浮层提示动画 */
.toast-enter-active {
animation: slideUp 0.3s ease-out;
}
.toast-leave-active {
animation: slideDown 0.3s ease-in;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
to {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
}
.tool-page {
height: calc(100vh - 64px);
display: flex;
flex-direction: column;
margin: -1rem;
padding: 0;
background: #ffffff;
}
.main-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
background: #ffffff;
}
/* 侧栏样式 */
.sidebar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 300px;
background: #ffffff;
border-right: 1px solid #e5e5e5;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 10;
display: flex;
flex-direction: column;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
}
.sidebar-open {
transform: translateX(0);
}
.content-wrapper.sidebar-pushed {
margin-left: 300px;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
background: #ffffff;
}
.sidebar-header h3 {
margin: 0;
font-size: 1rem;
color: #1a1a1a;
font-weight: 500;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666666;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
}
.close-btn:hover {
color: #1a1a1a;
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
background: #ffffff;
}
.empty-history {
padding: 2rem;
text-align: center;
color: #999999;
font-size: 0.875rem;
}
.history-item {
padding: 0.75rem;
margin-bottom: 0.5rem;
background: #ffffff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #e5e5e5;
}
.history-item:hover {
background: #f5f5f5;
border-color: #d0d0d0;
}
.history-header {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.25rem;
flex-wrap: wrap;
}
.history-type {
font-size: 0.75rem;
color: #666666;
background: #f5f5f5;
padding: 0.125rem 0.5rem;
border-radius: 3px;
font-weight: 500;
}
.history-encoding {
font-size: 0.75rem;
color: #666666;
background: #f0f9ff;
padding: 0.125rem 0.5rem;
border-radius: 3px;
font-weight: 500;
}
.history-time {
font-size: 0.75rem;
color: #999999;
margin-left: auto;
}
.history-preview {
font-size: 0.875rem;
color: #1a1a1a;
font-family: 'Courier New', monospace;
word-break: break-all;
}
.content-wrapper {
flex: 1;
display: flex;
position: relative;
background: #ffffff;
transition: margin-left 0.3s ease;
}
.left-panel,
.right-panel {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: #ffffff;
}
.left-panel {
position: relative;
}
/* 面板工具栏 */
.panel-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
background: #ffffff;
border-bottom: 1px solid #e5e5e5;
min-height: 40px;
}
.view-tabs {
display: flex;
gap: 0;
padding-left: 0.5rem;
}
.view-tab {
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: #666666;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.view-tab:hover {
color: #1a1a1a;
background: #f5f5f5;
}
.view-tab.active {
color: #ffffff;
background: #1a1a1a;
border-bottom-color: #1a1a1a;
}
.toolbar-actions {
display: flex;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
align-items: center;
}
.encoding-type-selector {
display: flex;
gap: 0.25rem;
margin-right: 0.5rem;
padding-right: 0.5rem;
border-right: 1px solid #e5e5e5;
}
.type-btn {
padding: 0.25rem 0.75rem;
border: 1px solid #e5e5e5;
border-radius: 3px;
background: transparent;
color: #666666;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.type-btn:hover {
background: #f5f5f5;
color: #1a1a1a;
border-color: #d0d0d0;
}
.type-btn.active {
background: #1a1a1a;
color: #ffffff;
border-color: #1a1a1a;
}
.toolbar-icon-btn {
padding: 0.375rem;
border: none;
background: transparent;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: #666666;
}
.toolbar-icon-btn:hover {
background: #f5f5f5;
color: #1a1a1a;
}
.toolbar-icon-btn:active {
background: #e5e5e5;
}
.toolbar-icon-btn svg,
.toolbar-icon-btn i {
display: block;
font-size: 14px;
}
.text-editor {
flex: 1;
width: 0;
min-width: 0;
padding: 1rem 1rem 1rem 3rem;
border: none;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: none;
outline: none;
background: #ffffff;
color: #1a1a1a;
line-height: 1.6;
overflow: hidden;
box-sizing: border-box;
}
.text-editor:focus {
background: #ffffff;
}
.text-editor::placeholder {
color: #999999;
}
.output-editor {
background: #fafafa;
color: #1a1a1a;
}
/* 分割线 */
.splitter {
width: 1px;
background: #e5e5e5;
cursor: col-resize;
position: relative;
flex-shrink: 0;
transition: background 0.2s;
}
.splitter:hover {
background: #d0d0d0;
width: 2px;
}
.splitter::before {
content: '';
position: absolute;
left: -2px;
right: -2px;
top: 0;
bottom: 0;
}
/* 侧栏切换按钮 */
.sidebar-toggle {
position: absolute;
left: 0;
bottom: 1rem;
z-index: 5;
}
.toggle-btn {
width: 32px;
height: 48px;
background: #1a1a1a;
color: #ffffff;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
}
.toggle-btn:hover {
background: #333333;
}
@media (max-width: 768px) {
.tool-page {
padding: 0;
margin: -1rem;
height: calc(100vh - 64px + 2rem);
}
.sidebar {
width: 80%;
max-width: 300px;
}
.left-panel,
.right-panel {
min-width: 0;
}
.view-tabs {
padding-left: 0.25rem;
}
.view-tab {
padding: 0.5rem 0.75rem;
font-size: 0.8125rem;
}
.encoding-type-selector {
margin-right: 0.25rem;
padding-right: 0.25rem;
}
.type-btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.text-editor {
padding-left: 2.5rem;
}
.toast-notification {
bottom: 10px;
left: 1rem;
right: 1rem;
transform: none;
min-width: auto;
max-width: none;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(20px);
}
}
}
</style>