init
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
// 最大输入限制:5MB(JSON格式化工具的限制)
|
||||
// 主要考虑因素:
|
||||
// 1. JSON.parse 可以处理更大的 JSON(10-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,10 +1039,22 @@ 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()
|
||||
resetEditorScroll()
|
||||
showToast('转义成功', 'info', 2000)
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user