Compare commits

..

3 Commits

Author SHA1 Message Date
renjue
ada0dfa3cc 修改REMOTE_DIR 2026-02-03 12:30:15 +08:00
renjue
2a07fd950f 编解码增加zlib 2026-02-02 16:19:44 +08:00
8e5eea02f1 init 2026-02-02 00:09:20 +08:00
10 changed files with 407 additions and 135 deletions

66
deploy.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -e
# 部署脚本:打包并通过 SSH 将构建产物同步到远程目录
# 用法: ./deploy.sh <服务器IP> <SSH端口> <登录账户> <登录密码> [sudo]
# 示例: ./deploy.sh 192.168.1.100 22 root mypassword
# 若远程目录无写权限加第5个参数 sudo先传到 /tmp再通过 sudo 拷到目标(会用到同一密码作为 sudo 密码)
# 示例: ./deploy.sh 192.168.1.100 22 myuser mypassword sudo
if [ $# -lt 4 ]; then
echo "用法: $0 <服务器IP> <SSH端口> <登录账户> <登录密码> [sudo]"
echo "示例: $0 192.168.1.100 22 root mypassword"
echo "远程目录无写权限时加第5参数: $0 <IP> <端口> <用户> <密码> sudo"
exit 1
fi
HOST="$1"
PORT="$2"
USER="$3"
PASSWORD="$4"
USE_SUDO="${5:-}"
REMOTE_DIR="/root/caddy/site/tool"
REMOTE_TMP="/tmp/tool_deploy_$$"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DIST_DIR="${SCRIPT_DIR}/dist"
SSH_OPTS="-p ${PORT} -o StrictHostKeyChecking=accept-new -T"
echo ">>> 正在打包..."
cd "$SCRIPT_DIR"
npm run build
if [ ! -d "$DIST_DIR" ]; then
echo "错误: 构建产物目录 dist 不存在"
exit 1
fi
# 检查 sshpass 是否可用(用于非交互式传入密码)
if ! command -v sshpass &> /dev/null; then
echo "未找到 sshpass请先安装"
echo " macOS: brew install sshpass"
echo " Ubuntu: sudo apt-get install sshpass"
exit 1
fi
export SSHPASS="$PASSWORD"
if [ "$USE_SUDO" = "sudo" ]; then
echo ">>> 正在同步到远程临时目录 ${REMOTE_TMP}"
sshpass -e rsync -avz --delete \
-e "ssh ${SSH_OPTS}" \
"${DIST_DIR}/" \
"${USER}@${HOST}:${REMOTE_TMP}/"
echo ">>> 正在用 sudo 拷贝到目标目录 ${REMOTE_DIR}"
# 只执行一次 sudo读一次密码在子 shell 里完成 rsync 与清理,避免第二次 sudo 无密码
printf '%s\n' "$PASSWORD" | sshpass -e ssh ${SSH_OPTS} "${USER}@${HOST}" \
"sudo -S sh -c 'rsync -avz --delete ${REMOTE_TMP}/ ${REMOTE_DIR}/ && rm -rf ${REMOTE_TMP}'"
else
echo ">>> 正在通过 SSH 同步到 ${USER}@${HOST}:${PORT} -> ${REMOTE_DIR}"
sshpass -e rsync -avz --delete \
-e "ssh ${SSH_OPTS}" \
"${DIST_DIR}/" \
"${USER}@${HOST}:${REMOTE_DIR}/"
fi
unset SSHPASS
echo ">>> 部署完成"

View File

@@ -6,7 +6,7 @@
<title>RC707的工具箱</title>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C2H4BGZJBD"></script>
<script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

View File

@@ -2,6 +2,7 @@
"name": "toolbox",
"version": "1.0.0",
"description": "A Vue-based toolbox application",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,6 +11,7 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0",
"fflate": "^0.8.2",
"qrcode": "^1.5.4",
"vue": "^3.4.0",
"vue-router": "^4.2.5"

View File

@@ -257,7 +257,13 @@ const currentColor = computed(() => {
return `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`
})
// RGB转十六进制
/**
* RGB转十六进制
* @param {number} r - 红色值 (0-255)
* @param {number} g - 绿色值 (0-255)
* @param {number} b - 蓝色值 (0-255)
* @returns {string} 十六进制颜色值(不含#号)
*/
function rgbToHex(r, g, b) {
const toHex = (n) => {
const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16)
@@ -266,7 +272,13 @@ function rgbToHex(r, g, b) {
return (toHex(r) + toHex(g) + toHex(b)).toUpperCase()
}
// RGB转HSL
/**
* RGB转HSL
* @param {number} r - 红色值 (0-255)
* @param {number} g - 绿色值 (0-255)
* @param {number} b - 蓝色值 (0-255)
* @returns {{h: number, s: number, l: number}} HSL对象
*/
function rgbToHsl(r, g, b) {
r /= 255
g /= 255
@@ -302,7 +314,11 @@ function rgbToHsl(r, g, b) {
}
}
// 十六进制转RGB
/**
* 十六进制转RGB
* @param {string} hex - 十六进制颜色值(不含#号)
* @returns {{r: number, g: number, b: number}|null} RGB对象或null
*/
function hexToRgb(hex) {
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
@@ -314,7 +330,13 @@ function hexToRgb(hex) {
: null
}
// HSL转RGB
/**
* HSL转RGB
* @param {number} h - 色相 (0-360)
* @param {number} s - 饱和度 (0-100)
* @param {number} l - 亮度 (0-100)
* @returns {{r: number, g: number, b: number}} RGB对象
*/
function hslToRgb(h, s, l) {
h /= 360
s /= 100
@@ -568,7 +590,6 @@ async function copyRgb() {
await navigator.clipboard.writeText(rgbText)
showToast('RGB已复制到剪贴板', 'info', 2000)
} catch (err) {
console.error('复制失败:', err)
showToast('复制失败:' + err.message)
}
}
@@ -651,7 +672,6 @@ async function pasteRgb() {
const text = await navigator.clipboard.readText()
processPastedText(text, 'rgb')
} catch (err) {
console.error('粘贴失败:', err)
// Clipboard API 失败,使用备用方法
currentPasteType.value = 'rgb'
pasteInputValue.value = ''
@@ -682,7 +702,6 @@ async function copyHex() {
await navigator.clipboard.writeText(hexText)
showToast('十六进制已复制到剪贴板', 'info', 2000)
} catch (err) {
console.error('复制失败:', err)
showToast('复制失败:' + err.message)
}
}
@@ -695,7 +714,6 @@ async function pasteHex() {
const text = await navigator.clipboard.readText()
processPastedText(text, 'hex')
} catch (err) {
console.error('粘贴失败:', err)
// Clipboard API 失败,使用备用方法
currentPasteType.value = 'hex'
pasteInputValue.value = ''
@@ -726,7 +744,6 @@ async function copyHsl() {
await navigator.clipboard.writeText(hslText)
showToast('HSL已复制到剪贴板', 'info', 2000)
} catch (err) {
console.error('复制失败:', err)
showToast('复制失败:' + err.message)
}
}
@@ -739,7 +756,6 @@ async function pasteHsl() {
const text = await navigator.clipboard.readText()
processPastedText(text, 'hsl')
} catch (err) {
console.error('粘贴失败:', err)
// Clipboard API 失败,使用备用方法
currentPasteType.value = 'hsl'
pasteInputValue.value = ''
@@ -779,7 +795,10 @@ function handlePasteEvent(event) {
}
}
// 保存到历史记录
/**
* 保存当前颜色到历史记录
* 自动去重最多保存50条记录
*/
function saveToHistory() {
const colorData = {
rgb: `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`,
@@ -803,7 +822,7 @@ function saveToHistory() {
history = JSON.parse(stored)
}
} catch (e) {
console.error('读取历史记录失败', e)
// 读取历史记录失败,忽略错误
}
// 检查是否与最后一条历史记录相同
@@ -828,11 +847,13 @@ function saveToHistory() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
loadHistoryList()
} catch (e) {
console.error('保存历史记录失败', e)
// 保存历史记录失败,忽略错误
}
}
// 加载历史记录列表
/**
* 从localStorage加载历史记录列表
*/
function loadHistoryList() {
try {
const stored = localStorage.getItem(STORAGE_KEY)
@@ -840,12 +861,15 @@ function loadHistoryList() {
historyList.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载历史记录失败', e)
// 加载历史记录失败,重置为空数组
historyList.value = []
}
}
// 加载历史记录
/**
* 加载历史记录到编辑器
* @param {Object} colorData - 颜色数据对象
*/
function loadHistory(colorData) {
updating.value = true
rgb.value = { ...colorData.rgbValues }
@@ -860,7 +884,11 @@ function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value
}
// 格式化时间
/**
* 格式化时间戳为相对时间或日期字符串
* @param {number} timestamp - 时间戳(毫秒)
* @returns {string} 格式化后的时间字符串
*/
function formatTime(timestamp) {
const date = new Date(timestamp)
const now = new Date()

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>
@@ -274,9 +274,12 @@
</template>
<script setup>
import {ref, computed, watch, onMounted, onUnmounted} from 'vue'
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
@@ -1271,7 +1289,6 @@ const compareListsIgnoreOrder = (listA, listB, ignoreOrder = false) => {
// 对比List数组- 寻找最佳匹配使相似度最高
const compareLists = (listA, listB, ignoreOrder = false) => {
const n = listA.length
const m = listB.length
@@ -1693,11 +1710,9 @@ const compareJson = (textA, textB) => {
// 执行节点对比,传递忽略列表顺序的参数
const comparison = compareJsonNodes(jsonA, jsonB, ignoreListOrder.value)
console.log(comparison)
// 转换为展示格式
const displayResult = convertComparisonToDisplay(comparison, 0)
console.log(displayResult)
// 计算统计信息
const stats = calculateStats(comparison)
@@ -1740,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)
@@ -1778,77 +1835,6 @@ const copyResult = async () => {
}
}
const compareTest = () => {
let total = {
left: [],
right: [],
stats: {same: 0, insert: 0, delete: 0, modify: 0}
}
const cases = [
["[]","[\"a\"]"],
["[\"a\"]", "[]"],
["[[\"a\",\"b\"]]", "[]"],
["[]","[\"a\",\"b\"]"],
["[]","[[\"a\",\"b\"]]"],
["{}","{\"a\": {\"c\":\"d\"}}"],
["{}", "{\"a\":\"b\"}"],
["[\"a\",\"b\"]", "[]"],
["{\"a\": {\"c\":\"d\"}}", "[]"],
["{\"a\":\"b\"}", "{}"],
["{\"a\":\"a\",\"b\":\"b\",\"d\":null}", "{\"a\":\"a\",\"c\":\"c\"}"],
["[\"a\",\"b\",null]", "[\"a\",\"c\"]"],
["[{\"a\":\"a\"},{\"b\":\"b\"},null]", "[{\"a\":\"a\"},{\"c\":\"c\"}]"],
["{\"a\":\"a\",\"b\":\"b\",\"c\":null}", "{\"a\":\"a\",\"c\":\"c\"}"],
["[{\"c\":\"d\"}]","[[\"c\",\"d\"]]"],
["{\"a\": {\"c\":\"d\"}}","{\"a\": [\"c\",\"d\"]}"],
["{\"a\": {\"c\":\"d\"}}","[\"c\",\"d\"]"],
["{\"a\": {\"c\":\"d\"}}","{\"a\": null}"],
["{\"a\": null}","{\"a\": {\"c\":\"d\"}}"],
["{\"a\": {\"c\":\"d\"}}","{}"],
["{\"a\":\"a1\"}", "{\"b\":\"b2\"}"],
["{\"a\":{\"c\":\"d\",\"e\":\"f\"}}", "{\"b\":{\"c\":\"d\",\"m\":\"l\"}}"],
["{\"a\":{\"c\":\"d\",\"e\":\"f\"}}", "{\"a\":{\"c\":\"d\",\"e\":\"l\"}}"],
["{\"a\": {\"c\":\"d\"}}", "{\"a\":[\"c\",\"d\"]}"],
["[{\"a\":\"a1\"},{\"b\":\"b2\"}]", "[{\"c\":\"b1\"},{\"a\":\"a1\"}]"],
["[{\"a\":\"a1\"},{\"b\":\"b2\"}]", "[{\"b\":\"b1\"},{\"a\":\"a1\"}]"],
["[{\"a\":\"a1\"},{\"b\":\"b2\"}]", "[{\"c\":\"b1\"},{\"a\":\"a1\"}]"],
["{\"a\":{\"c\":\"d\",\"e\":\"f\"},\"c\":\"a\",\"d\":\"e\",\"g\":[\"a\",\"b\",\"c\"],\"h\":[\"a\"],\"i\":[{\"a\":\"a1\"},{\"b\":\"b2\"},{\"c\":\"b1\"}]}", "{\"b\":{\"c\":\"d\",\"m\":\"l\"},\"c\":\"a\",\"d\":\"f\",\"g\":[\"a\",\"c\",\"b\"],\"h\":[\"b\",\"a\"],\"i\":[{\"b\":\"b1\"},{\"a\":\"a1\"},{\"d\":\"b1\"}]}"],
["[[[\"a\",\"b\"],[]]]","[[[\"a\",\"b\",\"c\"],[\"b\"]],[\"c\"]]"]
]
for (let item of cases) {
console.log("=================================================")
console.log(item)
console.log("=================================================")
let res = compareJson(item[0], item[1])
total.left.push({type: 'same', content: ''}, {
type: 'same',
content: '====================================='
}, {type: 'same', content: ''}, {type: 'same', content: item[0]}, {type: 'same', content: ''}, {
type: 'same',
content: '-----------------------------------'
})
total.right.push({type: 'same', content: ''}, {
type: 'same',
content: '====================================='
}, {type: 'same', content: ''}, {type: 'same', content: item[1]}, {type: 'same', content: ''}, {
type: 'same',
content: '-----------------------------------'
})
total.left.push({type: 'same', content: JSON.stringify(res.stats)}, {
type: 'same',
content: '-----------------------------------'
})
total.right.push({type: 'same', content: JSON.stringify(res.stats)}, {
type: 'same',
content: '-----------------------------------'
})
total.left.push(...res.left)
total.right.push(...res.right)
}
compareResult.value = total
}
// 切换侧栏
const toggleSidebar = () => {
sidebarOpen.value = !sidebarOpen.value
@@ -1912,7 +1898,7 @@ const saveToHistory = () => {
history = JSON.parse(stored)
}
} catch (e) {
console.error('读取历史记录失败', e)
// 读取历史记录失败,忽略错误
}
// 检查是否与最新记录相同,如果相同则不保存
@@ -1940,7 +1926,7 @@ const saveToHistory = () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
loadHistoryList()
} catch (e) {
console.error('保存历史记录失败', e)
// 保存历史记录失败,忽略错误
}
}
@@ -1952,7 +1938,7 @@ const loadHistoryList = () => {
historyList.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载历史记录失败', e)
// 加载历史记录失败,忽略错误
historyList.value = []
}
}
@@ -1973,7 +1959,6 @@ onMounted(() => {
updateLeftLineCount()
updateRightLineCount()
loadHistoryList()
// compareTest()
})
onUnmounted(() => {
@@ -2467,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

@@ -37,6 +37,7 @@
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>
@@ -77,6 +78,13 @@
>
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>
@@ -151,10 +159,11 @@
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { zlibSync, decompressSync } from 'fflate'
const inputText = ref('')
const outputText = ref('')
const encodingType = ref('base64') // 'base64'、'url''unicode'
const encodingType = ref('base64') // 'base64'、'url''unicode' 或 'zlib'
const leftPanelWidth = ref(50)
const rightPanelWidth = ref(50)
const isResizing = ref(false)
@@ -219,6 +228,26 @@ 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 = ''
@@ -321,6 +350,11 @@ const encode = () => {
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
@@ -352,6 +386,11 @@ const decode = () => {
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
@@ -368,6 +407,7 @@ const decode = () => {
const typeName = encodingType.value === 'base64' ? 'Base64' :
encodingType.value === 'url' ? 'URL' :
encodingType.value === 'unicode' ? 'Unicode' :
encodingType.value === 'zlib' ? 'Zlib' :
encodingType.value
showToast(`解码失败:请检查输入是否为有效的${typeName}编码字符串`)
outputText.value = ''
@@ -484,7 +524,7 @@ const saveToHistory = (item) => {
history = JSON.parse(stored)
}
} catch (e) {
console.error('读取历史记录失败', e)
// 读取历史记录失败,忽略错误
}
// 避免重复保存相同的记录
@@ -510,7 +550,7 @@ const saveToHistory = (item) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
loadHistoryList()
} catch (e) {
console.error('保存历史记录失败', e)
// 保存历史记录失败,忽略错误
}
}
@@ -522,7 +562,7 @@ const loadHistoryList = () => {
historyList.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载历史记录失败', e)
// 加载历史记录失败,重置为空数组
historyList.value = []
}
}

View File

@@ -1,8 +1,10 @@
<template>
<div class="home-container">
<div class="hero-section">
<h2 class="hero-title">今天是{{`${new Date().getFullYear()}${new Date().getMonth()+1}${new Date().getDate()}`}}</h2>
<p class="hero-subtitle">凭君莫话封侯事一将功成万骨枯</p>
<h2 class="hero-title">
今天是{{ `${new Date().getFullYear()}${new Date().getMonth() + 1}${new Date().getDate()}` }}
</h2>
<p id="jinrishici-sentence" class="hero-subtitle"></p>
</div>
<div class="tools-grid">
<router-link
@@ -22,7 +24,7 @@
</template>
<script setup>
import { ref } from 'vue'
import {onMounted, ref} from 'vue'
const tools = ref([
{
@@ -61,6 +63,14 @@ const tools = ref([
description: '颜色格式转换'
}
])
onMounted(() => {
let words_script = document.createElement('script');
words_script.charset = "utf-8";
words_script.src= "https://sdk.jinrishici.com/v2/browser/jinrishici.js";
document.body.appendChild(words_script);
})
</script>
<style scoped>
@@ -167,9 +177,6 @@ const tools = ref([
.tool-card {
flex: 0 0 100%;
max-width: 100%;
}
.tool-card {
padding: 1.5rem;
}
}

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)
@@ -602,7 +609,7 @@ const handleJsonPathInput = () => {
}
} catch (e) {
matchedPaths.value.clear()
console.error('JSONPath 解析错误:', e)
// JSONPath 解析错误,忽略
}
}
@@ -631,7 +638,7 @@ const loadJsonPathHistory = () => {
jsonPathHistory.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载 JSONPath 历史记录失败', e)
// 加载 JSONPath 历史记录失败,重置为空数组
jsonPathHistory.value = []
}
}
@@ -660,7 +667,7 @@ const saveJsonPathHistory = (jsonPath) => {
try {
localStorage.setItem(JSONPATH_HISTORY_KEY, JSON.stringify(jsonPathHistory.value))
} catch (e) {
console.error('保存 JSONPath 历史记录失败', e)
// 保存 JSONPath 历史记录失败,忽略错误
}
}
@@ -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))
@@ -1163,7 +1293,7 @@ const saveToHistory = (json) => {
history = JSON.parse(stored)
}
} catch (e) {
console.error('读取历史记录失败', e)
// 读取历史记录失败,忽略错误
}
// 添加到开头
@@ -1179,7 +1309,7 @@ const saveToHistory = (json) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
loadHistoryList()
} catch (e) {
console.error('保存历史记录失败', e)
// 保存历史记录失败,忽略错误
}
}
@@ -1191,7 +1321,7 @@ const loadHistoryList = () => {
historyList.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载历史记录失败', e)
// 加载历史记录失败,重置为空数组
historyList.value = []
}
}
@@ -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;

View File

@@ -221,7 +221,7 @@ const saveToHistory = (text) => {
history = JSON.parse(stored)
}
} catch (e) {
console.error('读取历史记录失败', e)
// 读取历史记录失败,忽略错误
}
// 避免重复保存相同的记录
@@ -243,7 +243,7 @@ const saveToHistory = (text) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
loadHistoryList()
} catch (e) {
console.error('保存历史记录失败', e)
// 保存历史记录失败,忽略错误
}
}
@@ -255,7 +255,7 @@ const loadHistoryList = () => {
historyList.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载历史记录失败', e)
// 加载历史记录失败,重置为空数组
historyList.value = []
}
}