This commit is contained in:
2026-01-30 22:20:01 +08:00
parent 309729dfcc
commit db120ceb14
2 changed files with 221 additions and 17 deletions

View File

@@ -98,7 +98,7 @@
<!-- 左侧输入框 -->
<div class="input-panel" :style="{ width: leftPanelWidth + '%' }">
<div class="panel-header">
<span class="panel-label">文本 A</span>
<span class="panel-label">文本 A <span class="size-limit">(最大 {{ maxInputLabel }})</span></span>
<div class="panel-actions">
<button @click="copyToClipboard('left')" class="icon-btn" title="复制">
<i class="far fa-copy"></i>
@@ -139,7 +139,7 @@
<!-- 右侧输入框 -->
<div class="input-panel" :style="{ width: rightPanelWidth + '%' }">
<div class="panel-header">
<span class="panel-label">文本 B</span>
<span class="panel-label">文本 B <span class="size-limit">(最大 {{ maxInputLabel }})</span></span>
<div class="panel-actions">
<button @click="copyToClipboard('right')" class="icon-btn" title="复制">
<i class="far fa-copy"></i>
@@ -276,7 +276,10 @@
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
const MAX_INPUT_BYTES = 500 * 1024 // 最大 500KB
// JSON对比模式最大 2MB受算法复杂度限制O(n*m) 动态规划)
// 文本对比模式:最大 5MB算法复杂度较低
const MAX_INPUT_BYTES_JSON = 2 * 1024 * 1024 // 2MB for JSON comparison
const MAX_INPUT_BYTES_TEXT = 5 * 1024 * 1024 // 5MB for text comparison
const leftText = ref('')
const rightText = ref('')
@@ -311,6 +314,15 @@ const leftFullscreenResultRef = ref(null)
const rightFullscreenResultRef = ref(null)
const isScrolling = ref(false)
// 计算当前模式的最大输入限制
const maxInputBytes = computed(() => {
return compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
})
const maxInputLabel = computed(() => {
return compareMode.value === 'json' ? '2MB' : '5MB'
})
// 提示消息
const toastMessage = ref('')
const toastType = ref('error')
@@ -353,9 +365,12 @@ const truncateToMaxBytes = (str, maxBytes) => {
// 应用输入大小限制,超出则截断并提示
const applyInputLimit = (side) => {
const ref = side === 'left' ? leftText : rightText
if (getByteLength(ref.value) <= MAX_INPUT_BYTES) return
ref.value = truncateToMaxBytes(ref.value, MAX_INPUT_BYTES)
showToast(`内容已超过 500KB 限制,已自动截断`, 'info', 3000)
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
if (getByteLength(ref.value) <= maxBytes) return
ref.value = truncateToMaxBytes(ref.value, maxBytes)
showToast(`内容已超过 ${maxBytesLabel} 限制,已自动截断`, 'info', 3000)
if (side === 'left') updateLeftLineCount()
else updateRightLineCount()
}
@@ -431,9 +446,12 @@ const pasteFromClipboard = async (side) => {
try {
let text = await navigator.clipboard.readText()
if (text.trim()) {
if (getByteLength(text) > MAX_INPUT_BYTES) {
text = truncateToMaxBytes(text, MAX_INPUT_BYTES)
showToast('粘贴内容已超过 500KB 限制,已自动截断', 'info', 3000)
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
if (getByteLength(text) > maxBytes) {
text = truncateToMaxBytes(text, maxBytes)
showToast(`粘贴内容已超过 ${maxBytesLabel} 限制,已自动截断`, 'info', 3000)
}
if (side === 'left') {
leftText.value = text
@@ -1737,6 +1755,48 @@ const performCompare = () => {
return
}
// 检查输入大小限制
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
const leftBytes = getByteLength(leftText.value)
const rightBytes = getByteLength(rightText.value)
if (leftBytes > maxBytes || rightBytes > maxBytes) {
showToast(`输入内容超过 ${maxBytesLabel} 限制,请减小输入大小`, 'error', 4000)
return
}
// JSON对比模式检查是否可能因复杂度过高导致性能问题
if (compareMode.value === 'json') {
try {
const jsonA = JSON.parse(leftText.value)
const jsonB = JSON.parse(rightText.value)
// 检查是否有大型数组(可能影响性能)
const checkLargeArray = (obj, depth = 0) => {
if (depth > 10) return false // 防止无限递归
if (Array.isArray(obj)) {
if (obj.length > 1000) return true
// 检查嵌套数组
for (const item of obj.slice(0, 10)) {
if (checkLargeArray(item, depth + 1)) return true
}
} else if (obj && typeof obj === 'object') {
for (const key in obj) {
if (checkLargeArray(obj[key], depth + 1)) return true
}
}
return false
}
if (checkLargeArray(jsonA) || checkLargeArray(jsonB)) {
showToast('检测到大型数组,对比可能需要较长时间,请耐心等待...', 'info', 5000)
}
} catch (e) {
// JSON解析失败会在下面处理
}
}
try {
if (compareMode.value === 'json') {
compareResult.value = compareJson(leftText.value, rightText.value)
@@ -2392,6 +2452,13 @@ onUnmounted(() => {
color: #1a1a1a;
}
.panel-label .size-limit {
font-size: 0.75rem;
font-weight: 400;
color: #999999;
margin-left: 0.25rem;
}
.panel-actions {
display: flex;
gap: 0.25rem;

View File

@@ -42,7 +42,7 @@
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
<div class="panel-toolbar">
<div class="view-tabs">
<button class="view-tab active">编辑器</button>
<button class="view-tab active">编辑器 <span class="size-limit">(最大 5MB)</span></button>
</div>
<div class="toolbar-actions">
<button @click="copyToClipboard" class="toolbar-icon-btn" title="复制">
@@ -199,6 +199,13 @@
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
import JsonTreeNode from '../components/JsonTreeNode.vue'
// 最大输入限制5MBJSON格式化工具的限制
// 主要考虑因素:
// 1. JSON.parse 可以处理更大的 JSON10-50MB
// 2. DOM 渲染是主要瓶颈:大型 JSON 会创建大量 DOM 节点
// 3. 路径遍历和展开/折叠状态管理也会消耗内存
const MAX_INPUT_BYTES = 5 * 1024 * 1024 // 5MB
const inputJson = ref('')
const sidebarOpen = ref(false)
const leftPanelWidth = ref(50)
@@ -699,8 +706,28 @@ const copyMatchedResults = async () => {
}
}
// 获取字符串 UTF-8 字节长度
const getByteLength = (str) => new TextEncoder().encode(str).length
// 将字符串截断到最大字节数(避免切断多字节字符)
const truncateToMaxBytes = (str, maxBytes) => {
if (getByteLength(str) <= maxBytes) return str
let end = str.length
while (end > 0 && getByteLength(str.slice(0, end)) > maxBytes) end--
return str.slice(0, end)
}
// 应用输入大小限制,超出则截断并提示
const applyInputLimit = () => {
if (getByteLength(inputJson.value) <= MAX_INPUT_BYTES) return
inputJson.value = truncateToMaxBytes(inputJson.value, MAX_INPUT_BYTES)
showToast('内容已超过 5MB 限制,已自动截断', 'info', 3000)
updateLineCount()
}
// 更新行号
const updateLineCount = () => {
applyInputLimit()
if (inputJson.value) {
lineCount.value = inputJson.value.split('\n').length
} else {
@@ -849,12 +876,21 @@ const getAllPaths = (obj, prefix = 'root') => {
// 监听输入变化,实时更新树形结构
watch(inputJson, () => {
// 先应用大小限制
applyInputLimit()
updateLineCount()
// 使用nextTick确保DOM更新后再调整高度
setTimeout(() => {
adjustTextareaHeight()
}, 0)
if (inputJson.value.trim()) {
// 检查大小,如果超过限制则不解析(避免性能问题)
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
treeLineCount.value = 1
return
}
try {
const parsed = JSON.parse(inputJson.value)
expandedNodes.value.clear()
@@ -879,9 +915,24 @@ const formatJson = () => {
showToast('请输入JSON数据')
return
}
// 检查输入大小
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
showToast(`输入内容超过 5MB 限制,无法格式化`, 'error', 4000)
return
}
try {
const parsed = JSON.parse(inputJson.value)
inputJson.value = JSON.stringify(parsed, null, 2)
const formatted = JSON.stringify(parsed, null, 2)
// 检查格式化后的大小
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
showToast('格式化后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
inputJson.value = formatted
updateLineCount()
resetEditorScroll()
showToast('格式化成功', 'info', 2000)
@@ -896,9 +947,24 @@ const minifyJson = () => {
showToast('请输入JSON数据')
return
}
// 检查输入大小
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
showToast(`输入内容超过 5MB 限制,无法压缩`, 'error', 4000)
return
}
try {
const parsed = JSON.parse(inputJson.value)
inputJson.value = JSON.stringify(parsed)
const minified = JSON.stringify(parsed)
// 检查压缩后的大小(压缩后应该更小,但为了安全还是检查)
if (getByteLength(minified) > MAX_INPUT_BYTES) {
showToast('压缩后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
inputJson.value = minified
updateLineCount()
resetEditorScroll()
showToast('压缩成功', 'info', 2000)
@@ -913,6 +979,13 @@ const escapeJson = () => {
showToast('请输入JSON数据')
return
}
// 检查输入大小
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
showToast(`输入内容超过 5MB 限制,无法转义`, 'error', 4000)
return
}
try {
let jsonToEscape = inputJson.value.trim()
@@ -966,8 +1039,20 @@ const escapeJson = () => {
// 不是JSON保持原样
}
// 添加引号并转义
inputJson.value = JSON.stringify(jsonToEscape)
const escaped = JSON.stringify(jsonToEscape)
// 检查转义后的大小
if (getByteLength(escaped) > MAX_INPUT_BYTES) {
showToast('转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
inputJson.value = escaped
}
}
// 最后检查一次大小(防止前面的分支没有检查)
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
showToast('转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
updateLineCount()
@@ -984,6 +1069,13 @@ const unescapeJson = () => {
showToast('请输入JSON数据')
return
}
// 检查输入大小
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
showToast(`输入内容超过 5MB 限制,无法取消转义`, 'error', 4000)
return
}
try {
let jsonToParse = inputJson.value.trim()
@@ -1027,13 +1119,24 @@ const unescapeJson = () => {
try {
const parsed = JSON.parse(unescaped)
// 如果解析成功,自动格式化
inputJson.value = JSON.stringify(parsed, null, 2)
const formatted = JSON.stringify(parsed, null, 2)
// 检查格式化后的大小
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
inputJson.value = formatted
updateLineCount()
resetEditorScroll()
showToast('取消转义并格式化成功', 'info', 2000)
return
} catch (e) {
// 如果解析失败,说明只是普通字符串,保持原样
// 检查字符串大小
if (getByteLength(unescaped) > MAX_INPUT_BYTES) {
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
inputJson.value = unescaped
updateLineCount()
resetEditorScroll()
@@ -1044,13 +1147,25 @@ const unescapeJson = () => {
// 如果取消转义后是对象或数组,自动格式化
if (typeof unescaped === 'object' && unescaped !== null) {
inputJson.value = JSON.stringify(unescaped, null, 2)
const formatted = JSON.stringify(unescaped, null, 2)
// 检查格式化后的大小
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
inputJson.value = formatted
updateLineCount()
resetEditorScroll()
showToast('取消转义并格式化成功', 'info', 2000)
} else {
// 其他类型(数字、布尔值等),转换为字符串
inputJson.value = String(unescaped)
const result = String(unescaped)
// 检查结果大小
if (getByteLength(result) > MAX_INPUT_BYTES) {
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
return
}
inputJson.value = result
updateLineCount()
resetEditorScroll()
showToast('取消转义成功', 'info', 2000)
@@ -1079,8 +1194,13 @@ const pasteFromClipboard = async () => {
// 优先使用现代 Clipboard API需要 HTTPS 或 localhost
if (navigator.clipboard && navigator.clipboard.readText) {
try {
const text = await navigator.clipboard.readText()
let text = await navigator.clipboard.readText()
if (text.trim()) {
// 检查大小限制
if (getByteLength(text) > MAX_INPUT_BYTES) {
text = truncateToMaxBytes(text, MAX_INPUT_BYTES)
showToast('粘贴内容已超过 5MB 限制,已自动截断', 'info', 3000)
}
inputJson.value = text
updateLineCount()
// 粘贴后不重置滚动位置,保持在当前位置
@@ -1129,6 +1249,16 @@ const handlePaste = async (event) => {
const pastedText = event.clipboardData?.getData('text') || ''
if (pastedText.trim()) {
// 检查大小限制
if (getByteLength(pastedText) > MAX_INPUT_BYTES) {
event.preventDefault()
const truncated = truncateToMaxBytes(pastedText, MAX_INPUT_BYTES)
inputJson.value = truncated
showToast('粘贴内容已超过 5MB 限制,已自动截断', 'info', 3000)
updateLineCount()
return
}
// 等待下一个tick确保inputJson已更新
await new Promise(resolve => setTimeout(resolve, 0))
@@ -1637,6 +1767,13 @@ onUnmounted(() => {
border-bottom-color: #1a1a1a;
}
.view-tab .size-limit {
font-size: 0.75rem;
font-weight: 400;
opacity: 0.7;
margin-left: 0.25rem;
}
.toolbar-actions {
display: flex;
gap: 0.25rem;