Compare commits
4 Commits
main
...
231019fcd1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
231019fcd1 | ||
|
|
788f79dd76 | ||
| 06020aa084 | |||
| 07cac667ae |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,4 +24,3 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.cursor
|
.cursor
|
||||||
/package-lock.json
|
|
||||||
|
|||||||
66
deploy.sh
66
deploy.sh
@@ -1,66 +0,0 @@
|
|||||||
#!/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 ">>> 部署完成"
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>RC707的工具箱</title>
|
<title>RC707的工具箱</title>
|
||||||
<!-- Google tag (gtag.js) -->
|
<!-- Google tag (gtag.js) -->
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C2H4BGZJBD"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C2H4BGZJBD"></script>
|
||||||
<script>
|
<script>
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag(){dataLayer.push(arguments);}
|
function gtag(){dataLayer.push(arguments);}
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|||||||
1516
package-lock.json
generated
Normal file
1516
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@
|
|||||||
"name": "toolbox",
|
"name": "toolbox",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A Vue-based toolbox application",
|
"description": "A Vue-based toolbox application",
|
||||||
"license": "MIT",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||||
"fflate": "^0.8.2",
|
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
|
|||||||
@@ -5,66 +5,42 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
component: Home,
|
component: Home
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/json-formatter',
|
path: '/json-formatter',
|
||||||
name: 'JsonFormatter',
|
name: 'JsonFormatter',
|
||||||
component: () => import('../views/JsonFormatter.vue'),
|
component: () => import('../views/JsonFormatter.vue')
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-JSON'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/comparator',
|
path: '/comparator',
|
||||||
name: 'Comparator',
|
name: 'Comparator',
|
||||||
component: () => import('../views/Comparator.vue'),
|
component: () => import('../views/Comparator.vue')
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-对比'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/encoder-decoder',
|
path: '/encoder-decoder',
|
||||||
name: 'EncoderDecoder',
|
name: 'EncoderDecoder',
|
||||||
component: () => import('../views/EncoderDecoder.vue'),
|
component: () => import('../views/EncoderDecoder.vue')
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-编解码'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/variable-name',
|
path: '/variable-name',
|
||||||
name: 'VariableNameConverter',
|
name: 'VariableNameConverter',
|
||||||
component: () => import('../views/VariableNameConverter.vue'),
|
component: () => import('../views/VariableNameConverter.vue')
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-变量名'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/qr-code',
|
path: '/qr-code',
|
||||||
name: 'QRCodeGenerator',
|
name: 'QRCodeGenerator',
|
||||||
component: () => import('../views/QRCodeGenerator.vue'),
|
component: () => import('../views/QRCodeGenerator.vue')
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-二维码'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/timestamp-converter',
|
path: '/timestamp-converter',
|
||||||
name: 'TimestampConverter',
|
name: 'TimestampConverter',
|
||||||
component: () => import('../views/TimestampConverter.vue'),
|
component: () => import('../views/TimestampConverter.vue')
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-时间戳'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/color-converter',
|
path: '/color-converter',
|
||||||
name: 'ColorConverter',
|
name: 'ColorConverter',
|
||||||
component: () => import('../views/ColorConverter.vue'),
|
component: () => import('../views/ColorConverter.vue')
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-颜色'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -73,13 +49,5 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
// 路由守卫:设置页面标题
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
if (to.meta.title) {
|
|
||||||
document.title = to.meta.title
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,6 @@
|
|||||||
<input
|
<input
|
||||||
v-model.number="rgb.r"
|
v-model.number="rgb.r"
|
||||||
@input="handleRgbInput"
|
@input="handleRgbInput"
|
||||||
@paste="handleRgbPaste"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="255"
|
max="255"
|
||||||
@@ -79,7 +78,6 @@
|
|||||||
<input
|
<input
|
||||||
v-model.number="rgb.g"
|
v-model.number="rgb.g"
|
||||||
@input="handleRgbInput"
|
@input="handleRgbInput"
|
||||||
@paste="handleRgbPaste"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="255"
|
max="255"
|
||||||
@@ -92,7 +90,6 @@
|
|||||||
<input
|
<input
|
||||||
v-model.number="rgb.b"
|
v-model.number="rgb.b"
|
||||||
@input="handleRgbInput"
|
@input="handleRgbInput"
|
||||||
@paste="handleRgbPaste"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="255"
|
max="255"
|
||||||
@@ -148,7 +145,6 @@
|
|||||||
<input
|
<input
|
||||||
v-model.number="hsl.h"
|
v-model.number="hsl.h"
|
||||||
@input="handleHslInput"
|
@input="handleHslInput"
|
||||||
@paste="handleHslPaste"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="360"
|
max="360"
|
||||||
@@ -161,7 +157,6 @@
|
|||||||
<input
|
<input
|
||||||
v-model.number="hsl.s"
|
v-model.number="hsl.s"
|
||||||
@input="handleHslInput"
|
@input="handleHslInput"
|
||||||
@paste="handleHslPaste"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
@@ -175,7 +170,6 @@
|
|||||||
<input
|
<input
|
||||||
v-model.number="hsl.l"
|
v-model.number="hsl.l"
|
||||||
@input="handleHslInput"
|
@input="handleHslInput"
|
||||||
@paste="handleHslPaste"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
@@ -257,13 +251,7 @@ const currentColor = computed(() => {
|
|||||||
return `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`
|
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) {
|
function rgbToHex(r, g, b) {
|
||||||
const toHex = (n) => {
|
const toHex = (n) => {
|
||||||
const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16)
|
const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16)
|
||||||
@@ -272,13 +260,7 @@ function rgbToHex(r, g, b) {
|
|||||||
return (toHex(r) + toHex(g) + toHex(b)).toUpperCase()
|
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) {
|
function rgbToHsl(r, g, b) {
|
||||||
r /= 255
|
r /= 255
|
||||||
g /= 255
|
g /= 255
|
||||||
@@ -314,11 +296,7 @@ function rgbToHsl(r, g, b) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 十六进制转RGB
|
||||||
* 十六进制转RGB
|
|
||||||
* @param {string} hex - 十六进制颜色值(不含#号)
|
|
||||||
* @returns {{r: number, g: number, b: number}|null} RGB对象或null
|
|
||||||
*/
|
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
return result
|
return result
|
||||||
@@ -330,13 +308,7 @@ function hexToRgb(hex) {
|
|||||||
: null
|
: 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) {
|
function hslToRgb(h, s, l) {
|
||||||
h /= 360
|
h /= 360
|
||||||
s /= 100
|
s /= 100
|
||||||
@@ -371,50 +343,6 @@ function hslToRgb(h, s, l) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理RGB粘贴事件
|
|
||||||
function handleRgbPaste(event) {
|
|
||||||
const pastedText = event.clipboardData?.getData('text') || ''
|
|
||||||
if (!pastedText || !pastedText.trim()) {
|
|
||||||
return // 如果没有粘贴内容,允许默认行为
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = pastedText.trim()
|
|
||||||
|
|
||||||
// 支持格式:rgb(255, 255, 255) 或 255, 255, 255
|
|
||||||
const rgbMatch = text.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i) ||
|
|
||||||
text.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
|
|
||||||
|
|
||||||
if (rgbMatch) {
|
|
||||||
event.preventDefault() // 阻止默认粘贴行为
|
|
||||||
|
|
||||||
const r = parseInt(rgbMatch[1])
|
|
||||||
const g = parseInt(rgbMatch[2])
|
|
||||||
const b = parseInt(rgbMatch[3])
|
|
||||||
|
|
||||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
||||||
updating.value = true
|
|
||||||
rgb.value = { r, g, b }
|
|
||||||
|
|
||||||
// 更新十六进制
|
|
||||||
hex.value = rgbToHex(r, g, b)
|
|
||||||
|
|
||||||
// 更新HSL
|
|
||||||
const hslValue = rgbToHsl(r, g, b)
|
|
||||||
hsl.value = hslValue
|
|
||||||
|
|
||||||
updating.value = false
|
|
||||||
|
|
||||||
// 保存到历史记录
|
|
||||||
saveToHistory()
|
|
||||||
|
|
||||||
showToast('RGB已粘贴并解析', 'info', 2000)
|
|
||||||
} else {
|
|
||||||
showToast('RGB值超出范围(0-255)', 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果不是RGB格式,允许默认粘贴行为(粘贴单个数字)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理RGB输入
|
// 处理RGB输入
|
||||||
function handleRgbInput() {
|
function handleRgbInput() {
|
||||||
if (updating.value) return
|
if (updating.value) return
|
||||||
@@ -476,65 +404,6 @@ function handleHexInput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理HSL粘贴事件
|
|
||||||
function handleHslPaste(event) {
|
|
||||||
const pastedText = event.clipboardData?.getData('text') || ''
|
|
||||||
if (!pastedText || !pastedText.trim()) {
|
|
||||||
return // 如果没有粘贴内容,允许默认行为
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = pastedText.trim()
|
|
||||||
|
|
||||||
// 支持格式:hsl(0, 0%, 100%) 或 0, 0%, 100% 或 0, 0, 100(不带%)
|
|
||||||
const hslMatchWithPercent = text.match(/hsl\s*\(\s*(\d+)\s*,\s*(\d+)\s*%\s*,\s*(\d+)\s*%\s*\)/i) ||
|
|
||||||
text.match(/(\d+)\s*,\s*(\d+)\s*%\s*,\s*(\d+)\s*%/)
|
|
||||||
const hslMatchWithoutPercent = text.match(/hsl\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i) ||
|
|
||||||
text.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
|
|
||||||
|
|
||||||
let hslMatch = hslMatchWithPercent || hslMatchWithoutPercent
|
|
||||||
const hasPercent = !!hslMatchWithPercent
|
|
||||||
|
|
||||||
if (hslMatch) {
|
|
||||||
event.preventDefault() // 阻止默认粘贴行为
|
|
||||||
|
|
||||||
let h = parseInt(hslMatch[1])
|
|
||||||
let s = parseInt(hslMatch[2])
|
|
||||||
let l = parseInt(hslMatch[3])
|
|
||||||
|
|
||||||
// 如果匹配的是不带%的格式,假设值是百分比(0-100)
|
|
||||||
// 如果值在合理范围内(0-100),直接使用;否则可能是小数格式(0-1),需要转换
|
|
||||||
if (!hasPercent) {
|
|
||||||
// 不带%的格式,如果值大于1,认为是百分比;否则认为是小数
|
|
||||||
if (s <= 1 && l <= 1) {
|
|
||||||
s = Math.round(s * 100)
|
|
||||||
l = Math.round(l * 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (h >= 0 && h <= 360 && s >= 0 && s <= 100 && l >= 0 && l <= 100) {
|
|
||||||
updating.value = true
|
|
||||||
hsl.value = { h, s, l }
|
|
||||||
|
|
||||||
// 更新RGB
|
|
||||||
const rgbValue = hslToRgb(h, s, l)
|
|
||||||
rgb.value = rgbValue
|
|
||||||
|
|
||||||
// 更新十六进制
|
|
||||||
hex.value = rgbToHex(rgb.value.r, rgb.value.g, rgb.value.b)
|
|
||||||
|
|
||||||
updating.value = false
|
|
||||||
|
|
||||||
// 保存到历史记录
|
|
||||||
saveToHistory()
|
|
||||||
|
|
||||||
showToast('HSL已粘贴并解析', 'info', 2000)
|
|
||||||
} else {
|
|
||||||
showToast('HSL值超出范围(H: 0-360, S/L: 0-100)', 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果不是HSL格式,允许默认粘贴行为(粘贴单个数字)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理HSL输入
|
// 处理HSL输入
|
||||||
function handleHslInput() {
|
function handleHslInput() {
|
||||||
if (updating.value) return
|
if (updating.value) return
|
||||||
@@ -590,6 +459,7 @@ async function copyRgb() {
|
|||||||
await navigator.clipboard.writeText(rgbText)
|
await navigator.clipboard.writeText(rgbText)
|
||||||
showToast('RGB已复制到剪贴板', 'info', 2000)
|
showToast('RGB已复制到剪贴板', 'info', 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err)
|
||||||
showToast('复制失败:' + err.message)
|
showToast('复制失败:' + err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -672,6 +542,7 @@ async function pasteRgb() {
|
|||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
processPastedText(text, 'rgb')
|
processPastedText(text, 'rgb')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('粘贴失败:', err)
|
||||||
// Clipboard API 失败,使用备用方法
|
// Clipboard API 失败,使用备用方法
|
||||||
currentPasteType.value = 'rgb'
|
currentPasteType.value = 'rgb'
|
||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
@@ -702,6 +573,7 @@ async function copyHex() {
|
|||||||
await navigator.clipboard.writeText(hexText)
|
await navigator.clipboard.writeText(hexText)
|
||||||
showToast('十六进制已复制到剪贴板', 'info', 2000)
|
showToast('十六进制已复制到剪贴板', 'info', 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err)
|
||||||
showToast('复制失败:' + err.message)
|
showToast('复制失败:' + err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -714,6 +586,7 @@ async function pasteHex() {
|
|||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
processPastedText(text, 'hex')
|
processPastedText(text, 'hex')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('粘贴失败:', err)
|
||||||
// Clipboard API 失败,使用备用方法
|
// Clipboard API 失败,使用备用方法
|
||||||
currentPasteType.value = 'hex'
|
currentPasteType.value = 'hex'
|
||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
@@ -744,6 +617,7 @@ async function copyHsl() {
|
|||||||
await navigator.clipboard.writeText(hslText)
|
await navigator.clipboard.writeText(hslText)
|
||||||
showToast('HSL已复制到剪贴板', 'info', 2000)
|
showToast('HSL已复制到剪贴板', 'info', 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err)
|
||||||
showToast('复制失败:' + err.message)
|
showToast('复制失败:' + err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,6 +630,7 @@ async function pasteHsl() {
|
|||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
processPastedText(text, 'hsl')
|
processPastedText(text, 'hsl')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('粘贴失败:', err)
|
||||||
// Clipboard API 失败,使用备用方法
|
// Clipboard API 失败,使用备用方法
|
||||||
currentPasteType.value = 'hsl'
|
currentPasteType.value = 'hsl'
|
||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
@@ -795,10 +670,7 @@ function handlePasteEvent(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 保存到历史记录
|
||||||
* 保存当前颜色到历史记录
|
|
||||||
* 自动去重,最多保存50条记录
|
|
||||||
*/
|
|
||||||
function saveToHistory() {
|
function saveToHistory() {
|
||||||
const colorData = {
|
const colorData = {
|
||||||
rgb: `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`,
|
rgb: `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`,
|
||||||
@@ -822,7 +694,7 @@ function saveToHistory() {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 读取历史记录失败,忽略错误
|
console.error('读取历史记录失败', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否与最后一条历史记录相同
|
// 检查是否与最后一条历史记录相同
|
||||||
@@ -847,13 +719,11 @@ function saveToHistory() {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 保存历史记录失败,忽略错误
|
console.error('保存历史记录失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 加载历史记录列表
|
||||||
* 从localStorage加载历史记录列表
|
|
||||||
*/
|
|
||||||
function loadHistoryList() {
|
function loadHistoryList() {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
@@ -861,15 +731,12 @@ function loadHistoryList() {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 加载历史记录失败,重置为空数组
|
console.error('加载历史记录失败', e)
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 加载历史记录
|
||||||
* 加载历史记录到编辑器
|
|
||||||
* @param {Object} colorData - 颜色数据对象
|
|
||||||
*/
|
|
||||||
function loadHistory(colorData) {
|
function loadHistory(colorData) {
|
||||||
updating.value = true
|
updating.value = true
|
||||||
rgb.value = { ...colorData.rgbValues }
|
rgb.value = { ...colorData.rgbValues }
|
||||||
@@ -884,11 +751,7 @@ function toggleSidebar() {
|
|||||||
sidebarOpen.value = !sidebarOpen.value
|
sidebarOpen.value = !sidebarOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 格式化时间
|
||||||
* 格式化时间戳为相对时间或日期字符串
|
|
||||||
* @param {number} timestamp - 时间戳(毫秒)
|
|
||||||
* @returns {string} 格式化后的时间字符串
|
|
||||||
*/
|
|
||||||
function formatTime(timestamp) {
|
function formatTime(timestamp) {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|||||||
@@ -15,33 +15,8 @@
|
|||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<div class="main-container">
|
<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-time">{{ formatTime(item.time) }}</div>
|
|
||||||
<div class="history-preview">
|
|
||||||
<div class="history-mode">{{ getModeLabel(item.compareMode, item.textSubMode, item.ignoreListOrder) }}</div>
|
|
||||||
<div class="history-text">{{ truncateText(item.leftText || '', 30) }} | {{ truncateText(item.rightText || '', 30) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主内容区域 -->
|
<!-- 主内容区域 -->
|
||||||
<div class="content-wrapper" :class="{ 'sidebar-pushed': sidebarOpen }">
|
<div class="content-wrapper">
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="mode-selector">
|
<div class="mode-selector">
|
||||||
@@ -76,12 +51,6 @@
|
|||||||
字符维度
|
字符维度
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="submode-selector" v-if="compareMode === 'json'">
|
|
||||||
<label class="switch-label">
|
|
||||||
<input type="checkbox" v-model="ignoreListOrder" class="switch-input" />
|
|
||||||
<span class="switch-text">忽略列表顺序</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button @click="performCompare" class="action-btn primary" title="开始对比">
|
<button @click="performCompare" class="action-btn primary" title="开始对比">
|
||||||
<i class="fas fa-code-compare"></i>
|
<i class="fas fa-code-compare"></i>
|
||||||
@@ -98,7 +67,7 @@
|
|||||||
<!-- 左侧输入框 -->
|
<!-- 左侧输入框 -->
|
||||||
<div class="input-panel" :style="{ width: leftPanelWidth + '%' }">
|
<div class="input-panel" :style="{ width: leftPanelWidth + '%' }">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-label">文本 A <span class="size-limit">(最大 {{ maxInputLabel }})</span></span>
|
<span class="panel-label">文本 A</span>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button @click="copyToClipboard('left')" class="icon-btn" title="复制">
|
<button @click="copyToClipboard('left')" class="icon-btn" title="复制">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
@@ -111,11 +80,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-toggle">
|
|
||||||
<button @click="toggleSidebar" class="toggle-btn">
|
|
||||||
{{ sidebarOpen ? '◀' : '▶' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<div class="line-numbers">
|
<div class="line-numbers">
|
||||||
<div v-for="n in leftLineCount" :key="n" class="line-number">{{ n }}</div>
|
<div v-for="n in leftLineCount" :key="n" class="line-number">{{ n }}</div>
|
||||||
@@ -139,7 +103,7 @@
|
|||||||
<!-- 右侧输入框 -->
|
<!-- 右侧输入框 -->
|
||||||
<div class="input-panel" :style="{ width: rightPanelWidth + '%' }">
|
<div class="input-panel" :style="{ width: rightPanelWidth + '%' }">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-label">文本 B <span class="size-limit">(最大 {{ maxInputLabel }})</span></span>
|
<span class="panel-label">文本 B</span>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button @click="copyToClipboard('right')" class="icon-btn" title="复制">
|
<button @click="copyToClipboard('right')" class="icon-btn" title="复制">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
@@ -247,7 +211,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-fullscreen-content" v-if="compareResult">
|
<div class="result-fullscreen-content" v-if="compareResult">
|
||||||
<div class="result-panel left-result result-fullscreen-panel" ref="leftFullscreenResultRef" @scroll="handleLeftFullscreenScroll">
|
<div class="result-panel left-result result-fullscreen-panel">
|
||||||
<div class="result-line" v-for="(line, index) in compareResult.left" :key="index">
|
<div class="result-line" v-for="(line, index) in compareResult.left" :key="index">
|
||||||
<span class="line-number">{{ line.lineNumber || index + 1 }}</span>
|
<span class="line-number">{{ line.lineNumber || index + 1 }}</span>
|
||||||
<span
|
<span
|
||||||
@@ -256,7 +220,7 @@
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-panel right-result result-fullscreen-panel" ref="rightFullscreenResultRef" @scroll="handleRightFullscreenScroll">
|
<div class="result-panel right-result result-fullscreen-panel">
|
||||||
<div class="result-line" v-for="(line, index) in compareResult.right" :key="index">
|
<div class="result-line" v-for="(line, index) in compareResult.right" :key="index">
|
||||||
<span class="line-number">{{ line.lineNumber || index + 1 }}</span>
|
<span class="line-number">{{ line.lineNumber || index + 1 }}</span>
|
||||||
<span
|
<span
|
||||||
@@ -274,18 +238,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
import {ref, computed, watch, onMounted, onUnmounted} from 'vue'
|
||||||
|
|
||||||
// JSON对比模式:最大 2MB(受算法复杂度限制,O(n*m) 动态规划)
|
const MAX_INPUT_BYTES = 500 * 1024 // 最大 500KB
|
||||||
// 文本对比模式:最大 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 leftText = ref('')
|
||||||
const rightText = ref('')
|
const rightText = ref('')
|
||||||
const compareMode = ref('text') // 'text' 或 'json'
|
const compareMode = ref('text') // 'text' 或 'json'
|
||||||
const textSubMode = ref('line') // 'line' | 'char'
|
const textSubMode = ref('line') // 'line' | 'char'
|
||||||
const ignoreListOrder = ref(false) // 是否忽略列表顺序
|
|
||||||
const leftPanelWidth = ref(50)
|
const leftPanelWidth = ref(50)
|
||||||
const rightPanelWidth = ref(50)
|
const rightPanelWidth = ref(50)
|
||||||
const isResizing = ref(false)
|
const isResizing = ref(false)
|
||||||
@@ -295,12 +255,6 @@ const leftEditorRef = ref(null)
|
|||||||
const rightEditorRef = ref(null)
|
const rightEditorRef = ref(null)
|
||||||
const compareResult = ref(null)
|
const compareResult = ref(null)
|
||||||
const resultFullscreen = ref(false)
|
const resultFullscreen = ref(false)
|
||||||
const sidebarOpen = ref(false)
|
|
||||||
|
|
||||||
// 历史记录
|
|
||||||
const historyList = ref([])
|
|
||||||
const STORAGE_KEY = 'comparator-history'
|
|
||||||
const MAX_HISTORY = 50
|
|
||||||
|
|
||||||
// 全屏时禁止页面滚动
|
// 全屏时禁止页面滚动
|
||||||
watch(resultFullscreen, (isFullscreen) => {
|
watch(resultFullscreen, (isFullscreen) => {
|
||||||
@@ -310,19 +264,8 @@ watch(resultFullscreen, (isFullscreen) => {
|
|||||||
const leftResultRef = ref(null)
|
const leftResultRef = ref(null)
|
||||||
const rightResultRef = ref(null)
|
const rightResultRef = ref(null)
|
||||||
const resultContentRef = ref(null)
|
const resultContentRef = ref(null)
|
||||||
const leftFullscreenResultRef = ref(null)
|
|
||||||
const rightFullscreenResultRef = ref(null)
|
|
||||||
const isScrolling = ref(false)
|
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 toastMessage = ref('')
|
||||||
const toastType = ref('error')
|
const toastType = ref('error')
|
||||||
@@ -365,12 +308,9 @@ const truncateToMaxBytes = (str, maxBytes) => {
|
|||||||
// 应用输入大小限制,超出则截断并提示
|
// 应用输入大小限制,超出则截断并提示
|
||||||
const applyInputLimit = (side) => {
|
const applyInputLimit = (side) => {
|
||||||
const ref = side === 'left' ? leftText : rightText
|
const ref = side === 'left' ? leftText : rightText
|
||||||
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
|
if (getByteLength(ref.value) <= MAX_INPUT_BYTES) return
|
||||||
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
|
ref.value = truncateToMaxBytes(ref.value, MAX_INPUT_BYTES)
|
||||||
|
showToast(`内容已超过 500KB 限制,已自动截断`, 'info', 3000)
|
||||||
if (getByteLength(ref.value) <= maxBytes) return
|
|
||||||
ref.value = truncateToMaxBytes(ref.value, maxBytes)
|
|
||||||
showToast(`内容已超过 ${maxBytesLabel} 限制,已自动截断`, 'info', 3000)
|
|
||||||
if (side === 'left') updateLeftLineCount()
|
if (side === 'left') updateLeftLineCount()
|
||||||
else updateRightLineCount()
|
else updateRightLineCount()
|
||||||
}
|
}
|
||||||
@@ -446,12 +386,9 @@ const pasteFromClipboard = async (side) => {
|
|||||||
try {
|
try {
|
||||||
let text = await navigator.clipboard.readText()
|
let text = await navigator.clipboard.readText()
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
|
if (getByteLength(text) > MAX_INPUT_BYTES) {
|
||||||
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
|
text = truncateToMaxBytes(text, MAX_INPUT_BYTES)
|
||||||
|
showToast('粘贴内容已超过 500KB 限制,已自动截断', 'info', 3000)
|
||||||
if (getByteLength(text) > maxBytes) {
|
|
||||||
text = truncateToMaxBytes(text, maxBytes)
|
|
||||||
showToast(`粘贴内容已超过 ${maxBytesLabel} 限制,已自动截断`, 'info', 3000)
|
|
||||||
}
|
}
|
||||||
if (side === 'left') {
|
if (side === 'left') {
|
||||||
leftText.value = text
|
leftText.value = text
|
||||||
@@ -501,7 +438,7 @@ const escapeHtml = (text) => {
|
|||||||
return div.innerHTML
|
return div.innerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步滚动处理(非全屏模式)
|
// 同步滚动处理
|
||||||
const handleLeftScroll = () => {
|
const handleLeftScroll = () => {
|
||||||
if (isScrolling.value) return
|
if (isScrolling.value) return
|
||||||
isScrolling.value = true
|
isScrolling.value = true
|
||||||
@@ -524,29 +461,6 @@ const handleRightScroll = () => {
|
|||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步滚动处理(全屏模式)
|
|
||||||
const handleLeftFullscreenScroll = () => {
|
|
||||||
if (isScrolling.value) return
|
|
||||||
isScrolling.value = true
|
|
||||||
if (leftFullscreenResultRef.value && rightFullscreenResultRef.value) {
|
|
||||||
rightFullscreenResultRef.value.scrollTop = leftFullscreenResultRef.value.scrollTop
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
isScrolling.value = false
|
|
||||||
}, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRightFullscreenScroll = () => {
|
|
||||||
if (isScrolling.value) return
|
|
||||||
isScrolling.value = true
|
|
||||||
if (leftFullscreenResultRef.value && rightFullscreenResultRef.value) {
|
|
||||||
leftFullscreenResultRef.value.scrollTop = rightFullscreenResultRef.value.scrollTop
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
isScrolling.value = false
|
|
||||||
}, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 高亮差异文本
|
// 高亮差异文本
|
||||||
const highlightDiff = (text, diffRanges) => {
|
const highlightDiff = (text, diffRanges) => {
|
||||||
if (!diffRanges || diffRanges.length === 0) {
|
if (!diffRanges || diffRanges.length === 0) {
|
||||||
@@ -1059,7 +973,7 @@ const NodeComparisonResult = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 对比两个JSON节点,返回对比结果和相似度
|
// 对比两个JSON节点,返回对比结果和相似度
|
||||||
const compareJsonNodes = (nodeA, nodeB, ignoreOrder = false) => {
|
const compareJsonNodes = (nodeA, nodeB) => {
|
||||||
const result = (type, similarity) => ({ type, similarity, nodeA, nodeB })
|
const result = (type, similarity) => ({ type, similarity, nodeA, nodeB })
|
||||||
|
|
||||||
if (isNullOrUndefined(nodeA) && isNullOrUndefined(nodeB)) {
|
if (isNullOrUndefined(nodeA) && isNullOrUndefined(nodeB)) {
|
||||||
@@ -1078,16 +992,13 @@ const compareJsonNodes = (nodeA, nodeB, ignoreOrder = false) => {
|
|||||||
if (typeof nodeA !== typeof nodeB) {
|
if (typeof nodeA !== typeof nodeB) {
|
||||||
return result(NodeComparisonResult.DIFFERENT, 0.0)
|
return result(NodeComparisonResult.DIFFERENT, 0.0)
|
||||||
}
|
}
|
||||||
if (isMapType(nodeA) && isMapType(nodeB)) return compareMaps(nodeA, nodeB, ignoreOrder)
|
if (isMapType(nodeA) && isMapType(nodeB)) return compareMaps(nodeA, nodeB)
|
||||||
if (isListType(nodeA) && isListType(nodeB)) {
|
if (isListType(nodeA) && isListType(nodeB)) return compareLists(nodeA, nodeB)
|
||||||
// 根据参数选择是否忽略列表顺序
|
|
||||||
return ignoreOrder ? compareListsIgnoreOrder(nodeA, nodeB, ignoreOrder) : compareLists(nodeA, nodeB, ignoreOrder)
|
|
||||||
}
|
|
||||||
return result(NodeComparisonResult.DIFFERENT, 0.0)
|
return result(NodeComparisonResult.DIFFERENT, 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对比Map(对象)
|
// 对比Map(对象)
|
||||||
const compareMaps = (mapA, mapB, ignoreOrder = false) => {
|
const compareMaps = (mapA, mapB) => {
|
||||||
const keysA = Object.keys(mapA).sort()
|
const keysA = Object.keys(mapA).sort()
|
||||||
const keysB = Object.keys(mapB).sort()
|
const keysB = Object.keys(mapB).sort()
|
||||||
const setA = new Set(keysA)
|
const setA = new Set(keysA)
|
||||||
@@ -1104,8 +1015,7 @@ const compareMaps = (mapA, mapB, ignoreOrder = false) => {
|
|||||||
const hasKeyB = setB.has(key)
|
const hasKeyB = setB.has(key)
|
||||||
const valueComparison = compareJsonNodes(
|
const valueComparison = compareJsonNodes(
|
||||||
hasKeyA ? mapA[key] : undefined,
|
hasKeyA ? mapA[key] : undefined,
|
||||||
hasKeyB ? mapB[key] : undefined,
|
hasKeyB ? mapB[key] : undefined
|
||||||
ignoreOrder
|
|
||||||
)
|
)
|
||||||
if (hasKeyA && hasKeyB && valueComparison.type === NodeComparisonResult.DIFFERENT) {
|
if (hasKeyA && hasKeyB && valueComparison.type === NodeComparisonResult.DIFFERENT) {
|
||||||
valueComparison.type = NodeComparisonResult.SIMILAR
|
valueComparison.type = NodeComparisonResult.SIMILAR
|
||||||
@@ -1139,156 +1049,9 @@ const compareMaps = (mapA, mapB, ignoreOrder = false) => {
|
|||||||
return { type: NodeComparisonResult.SIMILAR, similarity: avgSimilarity, ...base }
|
return { type: NodeComparisonResult.SIMILAR, similarity: avgSimilarity, ...base }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对比List(数组)- 忽略顺序,使用贪心算法寻找最佳匹配
|
|
||||||
const compareListsIgnoreOrder = (listA, listB, ignoreOrder = false) => {
|
|
||||||
const n = listA.length
|
|
||||||
const m = listB.length
|
|
||||||
|
|
||||||
// 计算所有元素对的相似度矩阵
|
|
||||||
const similarityMatrix = []
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
similarityMatrix[i] = []
|
|
||||||
for (let j = 0; j < m; j++) {
|
|
||||||
const comp = compareJsonNodes(listA[i], listB[j], ignoreOrder)
|
|
||||||
similarityMatrix[i][j] = comp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用贪心算法找到最佳匹配(不考虑顺序)
|
|
||||||
// 每次选择相似度最高的未匹配对
|
|
||||||
const matches = []
|
|
||||||
const usedA = new Set()
|
|
||||||
const usedB = new Set()
|
|
||||||
|
|
||||||
// 创建所有可能的匹配对,按相似度降序排序
|
|
||||||
const allPairs = []
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
for (let j = 0; j < m; j++) {
|
|
||||||
allPairs.push({
|
|
||||||
indexA: i,
|
|
||||||
indexB: j,
|
|
||||||
similarity: similarityMatrix[i][j].similarity,
|
|
||||||
comparison: similarityMatrix[i][j]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allPairs.sort((a, b) => b.similarity - a.similarity)
|
|
||||||
|
|
||||||
// 贪心匹配:选择相似度最高的未匹配对
|
|
||||||
for (const pair of allPairs) {
|
|
||||||
if (!usedA.has(pair.indexA) && !usedB.has(pair.indexB)) {
|
|
||||||
matches.push({
|
|
||||||
indexA: pair.indexA,
|
|
||||||
indexB: pair.indexB,
|
|
||||||
comparison: pair.comparison
|
|
||||||
})
|
|
||||||
usedA.add(pair.indexA)
|
|
||||||
usedB.add(pair.indexB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加未匹配的A中元素(删除)
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
if (!usedA.has(i)) {
|
|
||||||
matches.push({
|
|
||||||
indexA: i,
|
|
||||||
indexB: undefined,
|
|
||||||
comparison: {
|
|
||||||
type: NodeComparisonResult.DIFFERENT,
|
|
||||||
similarity: 0.0,
|
|
||||||
nodeA: listA[i],
|
|
||||||
nodeB: undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加未匹配的B中元素(插入)
|
|
||||||
for (let j = 0; j < m; j++) {
|
|
||||||
if (!usedB.has(j)) {
|
|
||||||
matches.push({
|
|
||||||
indexA: undefined,
|
|
||||||
indexB: j,
|
|
||||||
comparison: {
|
|
||||||
type: NodeComparisonResult.DIFFERENT,
|
|
||||||
similarity: 0.0,
|
|
||||||
nodeA: undefined,
|
|
||||||
nodeB: listB[j]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按原始顺序排序匹配结果(先A后B,保持展示的一致性)
|
|
||||||
matches.sort((a, b) => {
|
|
||||||
if (a.indexA !== undefined && b.indexA !== undefined) {
|
|
||||||
return a.indexA - b.indexA
|
|
||||||
}
|
|
||||||
if (a.indexA !== undefined) return -1
|
|
||||||
if (b.indexA !== undefined) return 1
|
|
||||||
if (a.indexB !== undefined && b.indexB !== undefined) {
|
|
||||||
return a.indexB - b.indexB
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断父节点类型
|
|
||||||
const totalElements = n + m
|
|
||||||
const matchedElements = matches.filter(m => m.indexA !== undefined && m.indexB !== undefined).length
|
|
||||||
const sameMatches = matches.filter(m =>
|
|
||||||
m.indexA !== undefined &&
|
|
||||||
m.indexB !== undefined &&
|
|
||||||
m.comparison.type === NodeComparisonResult.SAME
|
|
||||||
).length
|
|
||||||
const differentMatches = matches.filter(m =>
|
|
||||||
m.comparison.type === NodeComparisonResult.DIFFERENT
|
|
||||||
).length
|
|
||||||
|
|
||||||
if (totalElements === 0) {
|
|
||||||
return {
|
|
||||||
type: NodeComparisonResult.SAME,
|
|
||||||
similarity: 1.0,
|
|
||||||
nodeA: listA,
|
|
||||||
nodeB: listB,
|
|
||||||
matches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n !== 0 && m !== 0 && differentMatches === totalElements) {
|
|
||||||
return {
|
|
||||||
type: NodeComparisonResult.DIFFERENT,
|
|
||||||
similarity: 0.0,
|
|
||||||
nodeA: listA,
|
|
||||||
nodeB: listB,
|
|
||||||
matches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sameMatches === matchedElements && matchedElements === totalElements) {
|
|
||||||
return {
|
|
||||||
type: NodeComparisonResult.SAME,
|
|
||||||
similarity: 1.0,
|
|
||||||
nodeA: listA,
|
|
||||||
nodeB: listB,
|
|
||||||
matches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算加权平均相似度
|
|
||||||
const totalSimilarity = matches.reduce((sum, m) => sum + m.comparison.similarity, 0)
|
|
||||||
const avgSimilarity = totalElements > 0 ? totalSimilarity / totalElements : 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: NodeComparisonResult.SIMILAR,
|
|
||||||
similarity: avgSimilarity,
|
|
||||||
nodeA: listA,
|
|
||||||
nodeB: listB,
|
|
||||||
matches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对比List(数组)- 寻找最佳匹配使相似度最高
|
// 对比List(数组)- 寻找最佳匹配使相似度最高
|
||||||
const compareLists = (listA, listB, ignoreOrder = false) => {
|
const compareLists = (listA, listB) => {
|
||||||
|
|
||||||
const n = listA.length
|
const n = listA.length
|
||||||
const m = listB.length
|
const m = listB.length
|
||||||
|
|
||||||
@@ -1297,7 +1060,7 @@ const compareLists = (listA, listB, ignoreOrder = false) => {
|
|||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
similarityMatrix[i] = []
|
similarityMatrix[i] = []
|
||||||
for (let j = 0; j < m; j++) {
|
for (let j = 0; j < m; j++) {
|
||||||
const comp = compareJsonNodes(listA[i], listB[j], ignoreOrder)
|
const comp = compareJsonNodes(listA[i], listB[j])
|
||||||
similarityMatrix[i][j] = comp
|
similarityMatrix[i][j] = comp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1307,15 +1070,6 @@ const compareLists = (listA, listB, ignoreOrder = false) => {
|
|||||||
const dp = Array(n + 1).fill(null).map(() => Array(m + 1).fill(0))
|
const dp = Array(n + 1).fill(null).map(() => Array(m + 1).fill(0))
|
||||||
const path = Array(n + 1).fill(null).map(() => Array(m + 1).fill(null))
|
const path = Array(n + 1).fill(null).map(() => Array(m + 1).fill(null))
|
||||||
|
|
||||||
// 初始化边界情况:当j=0时,只能跳过A中的元素
|
|
||||||
for (let i = 1; i <= n; i++) {
|
|
||||||
path[i][0] = 'skipA'
|
|
||||||
}
|
|
||||||
// 初始化边界情况:当i=0时,只能跳过B中的元素
|
|
||||||
for (let j = 1; j <= m; j++) {
|
|
||||||
path[0][j] = 'skipB'
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i <= n; i++) {
|
for (let i = 1; i <= n; i++) {
|
||||||
for (let j = 1; j <= m; j++) {
|
for (let j = 1; j <= m; j++) {
|
||||||
const matchSimilarity = similarityMatrix[i - 1][j - 1].similarity
|
const matchSimilarity = similarityMatrix[i - 1][j - 1].similarity
|
||||||
@@ -1400,7 +1154,7 @@ const compareLists = (listA, listB, ignoreOrder = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (n !== 0 && m !== 0 && differentMatches === totalElements) {
|
if (differentMatches === totalElements) {
|
||||||
return {
|
return {
|
||||||
type: NodeComparisonResult.DIFFERENT,
|
type: NodeComparisonResult.DIFFERENT,
|
||||||
similarity: 0.0,
|
similarity: 0.0,
|
||||||
@@ -1708,11 +1462,13 @@ const compareJson = (textA, textB) => {
|
|||||||
const jsonA = JSON.parse(textA)
|
const jsonA = JSON.parse(textA)
|
||||||
const jsonB = JSON.parse(textB)
|
const jsonB = JSON.parse(textB)
|
||||||
|
|
||||||
// 执行节点对比,传递忽略列表顺序的参数
|
// 执行节点对比
|
||||||
const comparison = compareJsonNodes(jsonA, jsonB, ignoreListOrder.value)
|
const comparison = compareJsonNodes(jsonA, jsonB)
|
||||||
|
console.log(comparison)
|
||||||
|
|
||||||
// 转换为展示格式
|
// 转换为展示格式
|
||||||
const displayResult = convertComparisonToDisplay(comparison, 0)
|
const displayResult = convertComparisonToDisplay(comparison, 0)
|
||||||
|
console.log(displayResult)
|
||||||
|
|
||||||
// 计算统计信息
|
// 计算统计信息
|
||||||
const stats = calculateStats(comparison)
|
const stats = calculateStats(comparison)
|
||||||
@@ -1755,48 +1511,6 @@ const performCompare = () => {
|
|||||||
return
|
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 {
|
try {
|
||||||
if (compareMode.value === 'json') {
|
if (compareMode.value === 'json') {
|
||||||
compareResult.value = compareJson(leftText.value, rightText.value)
|
compareResult.value = compareJson(leftText.value, rightText.value)
|
||||||
@@ -1808,8 +1522,6 @@ const performCompare = () => {
|
|||||||
compareResult.value = compareTextByChar(leftText.value, rightText.value)
|
compareResult.value = compareTextByChar(leftText.value, rightText.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 保存到历史记录
|
|
||||||
saveToHistory()
|
|
||||||
showToast('对比完成', 'info', 2000)
|
showToast('对比完成', 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message)
|
showToast(e.message)
|
||||||
@@ -1835,130 +1547,117 @@ const copyResult = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换侧栏
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
sidebarOpen.value = !sidebarOpen.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化时间
|
const compareTest = () => {
|
||||||
const formatTime = (timestamp) => {
|
let total = {
|
||||||
const date = new Date(timestamp)
|
left: [],
|
||||||
const now = new Date()
|
right: [],
|
||||||
const diff = now - date
|
stats: {same: 0, insert: 0, delete: 0, modify: 0}
|
||||||
|
|
||||||
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) return ''
|
|
||||||
if (text.length <= maxLength) return text
|
|
||||||
return text.substring(0, maxLength) + '...'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取模式标签
|
|
||||||
const getModeLabel = (mode, subMode, ignoreOrder) => {
|
|
||||||
if (mode === 'json') {
|
|
||||||
return ignoreOrder ? 'JSON对比(忽略列表顺序)' : 'JSON对比'
|
|
||||||
} else {
|
|
||||||
return subMode === 'line' ? '文本对比(行维度)' : '文本对比(字符维度)'
|
|
||||||
}
|
}
|
||||||
}
|
const cases = [
|
||||||
|
// 1. 完全相同的简单JSON
|
||||||
// 保存到历史记录
|
["{\"name\":\"John\",\"age\":30}", "{\"name\":\"John\",\"age\":30}"],
|
||||||
const saveToHistory = () => {
|
// 2. 值不同的简单JSON
|
||||||
// 如果两个输入框都为空,不保存
|
["{\"name\":\"John\",\"age\":30}", "{\"name\":\"Jane\",\"age\":25}"],
|
||||||
if (!leftText.value.trim() && !rightText.value.trim()) {
|
// 3. 键不同
|
||||||
return
|
["{\"a\":1,\"b\":2}", "{\"a\":1,\"c\":3}"],
|
||||||
|
// 4. 嵌套对象
|
||||||
|
["{\"user\":{\"name\":\"John\",\"address\":{\"city\":\"NY\"}}}", "{\"user\":{\"name\":\"Jane\",\"address\":{\"city\":\"LA\"}}}"],
|
||||||
|
// 5. 嵌套数组
|
||||||
|
["{\"items\":[1,2,[3,4]]}", "{\"items\":[1,2,[3,5]]}"],
|
||||||
|
// 6. 数组顺序不同(是否考虑顺序)
|
||||||
|
["[1,2,3,4]", "[4,3,2,1]"],
|
||||||
|
// 7. 数组长度不同
|
||||||
|
["[1,2,3,4,5]", "[1,2,3]"],
|
||||||
|
// 8. 空数组
|
||||||
|
["[]", "[]"],
|
||||||
|
["[1,2,3]", "[]"],
|
||||||
|
// 10. null值处理
|
||||||
|
["{\"a\":null,\"b\":\"value\"}", "{\"a\":null,\"b\":\"value\"}"],
|
||||||
|
["{\"a\":null}", "{\"a\":\"null\"}"], // null vs 字符串"null"
|
||||||
|
// 11. 布尔值
|
||||||
|
["{\"flag\":true,\"active\":false}", "{\"flag\":true,\"active\":true}"],
|
||||||
|
// 12. 数字类型(整数、浮点数、科学计数法)
|
||||||
|
["{\"int\":42,\"float\":3.14}", "{\"int\":42,\"float\":3.140}"],
|
||||||
|
["{\"num\":1e3}", "{\"num\":1000}"],
|
||||||
|
// 13. 空字符串
|
||||||
|
["{\"str\":\"\",\"normal\":\"text\"}", "{\"str\":\"\",\"normal\":\"text\"}"],
|
||||||
|
// 14. 空对象
|
||||||
|
["{}", "{}"],
|
||||||
|
["{}", "{\"key\":\"value\"}"],
|
||||||
|
// 15. 键的顺序不同
|
||||||
|
["{\"a\":1,\"b\":2}", "{\"b\":2,\"a\":1}"], // 对象键顺序是否影响比较
|
||||||
|
// 16. 数据类型混用
|
||||||
|
["{\"id\":\"123\"}", "{\"id\":123}"], // 字符串数字 vs 数字
|
||||||
|
["{\"value\":true}", "{\"value\":\"true\"}"], // 布尔 vs 字符串
|
||||||
|
// 17. 特殊字符
|
||||||
|
["{\"text\":\"line1\\nline2\\ttab\"}", "{\"text\":\"line1\\nline2\\ttab\"}"],
|
||||||
|
["{\"quote\":\"He said \\\"hello\\\"\"}", "{\"quote\":\"He said \\\"hello\\\"\"}"],
|
||||||
|
["{\"unicode\":\"\u4e2d\u6587\"}", "{\"unicode\":\"中文\"}"],
|
||||||
|
// 18. 大数字
|
||||||
|
["{\"big\":9999999999999999}", "{\"big\":10000000000000000}"],
|
||||||
|
["{\"big\":9007199254740991}", "{\"big\":9007199254740992}"], // 超出安全整数范围
|
||||||
|
// 19. 多层嵌套混合
|
||||||
|
["{\"users\":[{\"id\":1,\"data\":{\"scores\":[85,90,null]}},{\"id\":2}]}", "{\"users\":[{\"id\":1,\"data\":{\"scores\":[85,90,95]}},{\"id\":2}]}"],
|
||||||
|
// 21. 日期对象(通常序列化为字符串)
|
||||||
|
["{\"date\":\"2024-01-15T10:30:00Z\"}", "{\"date\":\"2024-01-15T10:30:00.000Z\"}"],
|
||||||
|
["{\"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\": {\"c\":\"d\"}}"],
|
||||||
|
["{}", "{\"a\":\"b\"}"],
|
||||||
|
["{\"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(item)
|
||||||
|
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 historyItem = {
|
|
||||||
leftText: leftText.value,
|
|
||||||
rightText: rightText.value,
|
|
||||||
compareMode: compareMode.value,
|
|
||||||
textSubMode: textSubMode.value,
|
|
||||||
ignoreListOrder: ignoreListOrder.value,
|
|
||||||
time: Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从localStorage读取现有历史
|
|
||||||
let history = []
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
|
||||||
if (stored) {
|
|
||||||
history = JSON.parse(stored)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 读取历史记录失败,忽略错误
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否与最新记录相同,如果相同则不保存
|
|
||||||
if (history.length > 0) {
|
|
||||||
const lastItem = history[0]
|
|
||||||
if (lastItem.leftText === historyItem.leftText &&
|
|
||||||
lastItem.rightText === historyItem.rightText &&
|
|
||||||
lastItem.compareMode === historyItem.compareMode &&
|
|
||||||
lastItem.textSubMode === historyItem.textSubMode &&
|
|
||||||
lastItem.ignoreListOrder === historyItem.ignoreListOrder) {
|
|
||||||
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) => {
|
|
||||||
leftText.value = item.leftText || ''
|
|
||||||
rightText.value = item.rightText || ''
|
|
||||||
compareMode.value = item.compareMode || 'text'
|
|
||||||
textSubMode.value = item.textSubMode || 'line'
|
|
||||||
ignoreListOrder.value = item.ignoreListOrder || false
|
|
||||||
updateLeftLineCount()
|
|
||||||
updateRightLineCount()
|
|
||||||
compareResult.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateLeftLineCount()
|
updateLeftLineCount()
|
||||||
updateRightLineCount()
|
updateRightLineCount()
|
||||||
loadHistoryList()
|
// compareTest()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -2108,127 +1807,12 @@ onUnmounted(() => {
|
|||||||
background: #ffffff;
|
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-time {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #999999;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-preview {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-mode {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a1a1a;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-text {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
word-break: break-all;
|
|
||||||
color: #666666;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
transition: margin-left 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 工具栏 */
|
/* 工具栏 */
|
||||||
@@ -2302,57 +1886,6 @@ onUnmounted(() => {
|
|||||||
border-color: #1a1a1a;
|
border-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-input {
|
|
||||||
width: 36px;
|
|
||||||
height: 20px;
|
|
||||||
appearance: none;
|
|
||||||
background: #e5e5e5;
|
|
||||||
border-radius: 10px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-input:checked {
|
|
||||||
background: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-input::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #ffffff;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-input:checked::before {
|
|
||||||
transform: translateX(16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-text {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: #666666;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-input:checked + .switch-text {
|
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-actions {
|
.toolbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -2407,34 +1940,6 @@ onUnmounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
@@ -2452,13 +1957,6 @@ onUnmounted(() => {
|
|||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-label .size-limit {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #999999;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-actions {
|
.panel-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
@@ -2834,11 +2332,6 @@ onUnmounted(() => {
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 80%;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notification {
|
.toast-notification {
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
item.encodingType === 'base64' ? 'Base64' :
|
item.encodingType === 'base64' ? 'Base64' :
|
||||||
item.encodingType === 'url' ? 'URL' :
|
item.encodingType === 'url' ? 'URL' :
|
||||||
item.encodingType === 'unicode' ? 'Unicode' :
|
item.encodingType === 'unicode' ? 'Unicode' :
|
||||||
item.encodingType === 'zlib' ? 'Zlib' :
|
|
||||||
item.encodingType
|
item.encodingType
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="history-time">{{ formatTime(item.time) }}</span>
|
<span class="history-time">{{ formatTime(item.time) }}</span>
|
||||||
@@ -78,13 +77,6 @@
|
|||||||
>
|
>
|
||||||
Unicode
|
Unicode
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
@click="encodingType = 'zlib'"
|
|
||||||
:class="['type-btn', { active: encodingType === 'zlib' }]"
|
|
||||||
title="Zlib 压缩/解压"
|
|
||||||
>
|
|
||||||
Zlib
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button @click="copyInputToClipboard" class="toolbar-icon-btn" title="复制">
|
<button @click="copyInputToClipboard" class="toolbar-icon-btn" title="复制">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
@@ -159,11 +151,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { zlibSync, decompressSync } from 'fflate'
|
|
||||||
|
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
const outputText = ref('')
|
const outputText = ref('')
|
||||||
const encodingType = ref('base64') // 'base64'、'url'、'unicode' 或 'zlib'
|
const encodingType = ref('base64') // 'base64'、'url' 或 'unicode'
|
||||||
const leftPanelWidth = ref(50)
|
const leftPanelWidth = ref(50)
|
||||||
const rightPanelWidth = ref(50)
|
const rightPanelWidth = ref(50)
|
||||||
const isResizing = ref(false)
|
const isResizing = ref(false)
|
||||||
@@ -228,26 +219,6 @@ watch(() => outputText.value, () => {
|
|||||||
updateOutputLineCount()
|
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 编码
|
// Unicode 编码
|
||||||
const encodeUnicode = (text) => {
|
const encodeUnicode = (text) => {
|
||||||
let result = ''
|
let result = ''
|
||||||
@@ -350,11 +321,6 @@ const encode = () => {
|
|||||||
result = encodeURIComponent(inputText.value)
|
result = encodeURIComponent(inputText.value)
|
||||||
} else if (encodingType.value === 'unicode') {
|
} else if (encodingType.value === 'unicode') {
|
||||||
result = encodeUnicode(inputText.value)
|
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
|
outputText.value = result
|
||||||
|
|
||||||
@@ -386,11 +352,6 @@ const decode = () => {
|
|||||||
result = decodeURIComponent(inputText.value)
|
result = decodeURIComponent(inputText.value)
|
||||||
} else if (encodingType.value === 'unicode') {
|
} else if (encodingType.value === 'unicode') {
|
||||||
result = decodeUnicode(inputText.value)
|
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
|
outputText.value = result
|
||||||
|
|
||||||
@@ -407,7 +368,6 @@ const decode = () => {
|
|||||||
const typeName = encodingType.value === 'base64' ? 'Base64' :
|
const typeName = encodingType.value === 'base64' ? 'Base64' :
|
||||||
encodingType.value === 'url' ? 'URL' :
|
encodingType.value === 'url' ? 'URL' :
|
||||||
encodingType.value === 'unicode' ? 'Unicode' :
|
encodingType.value === 'unicode' ? 'Unicode' :
|
||||||
encodingType.value === 'zlib' ? 'Zlib' :
|
|
||||||
encodingType.value
|
encodingType.value
|
||||||
showToast(`解码失败:请检查输入是否为有效的${typeName}编码字符串`)
|
showToast(`解码失败:请检查输入是否为有效的${typeName}编码字符串`)
|
||||||
outputText.value = ''
|
outputText.value = ''
|
||||||
@@ -524,7 +484,7 @@ const saveToHistory = (item) => {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 读取历史记录失败,忽略错误
|
console.error('读取历史记录失败', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复保存相同的记录
|
// 避免重复保存相同的记录
|
||||||
@@ -550,7 +510,7 @@ const saveToHistory = (item) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 保存历史记录失败,忽略错误
|
console.error('保存历史记录失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +522,7 @@ const loadHistoryList = () => {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 加载历史记录失败,重置为空数组
|
console.error('加载历史记录失败', e)
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
<div class="hero-section">
|
<div class="hero-section">
|
||||||
<h2 class="hero-title">
|
<h2 class="hero-title">今天是{{`${new Date().getFullYear()}年${new Date().getMonth()+1}月${new Date().getDate()}日`}}</h2>
|
||||||
今天是{{ `${new Date().getFullYear()}年${new Date().getMonth() + 1}月${new Date().getDate()}日` }}
|
<p class="hero-subtitle">凭君莫话封侯事,一将功成万骨枯。</p>
|
||||||
</h2>
|
|
||||||
<p id="jinrishici-sentence" class="hero-subtitle"></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tools-grid">
|
<div class="tools-grid">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -24,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const tools = ref([
|
const tools = ref([
|
||||||
{
|
{
|
||||||
@@ -63,14 +61,6 @@ const tools = ref([
|
|||||||
description: '颜色格式转换'
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -177,6 +167,9 @@ onMounted(() => {
|
|||||||
.tool-card {
|
.tool-card {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
|
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
|
||||||
<div class="panel-toolbar">
|
<div class="panel-toolbar">
|
||||||
<div class="view-tabs">
|
<div class="view-tabs">
|
||||||
<button class="view-tab active">编辑器 <span class="size-limit">(最大 5MB)</span></button>
|
<button class="view-tab active">编辑器</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button @click="copyToClipboard" class="toolbar-icon-btn" title="复制">
|
<button @click="copyToClipboard" class="toolbar-icon-btn" title="复制">
|
||||||
@@ -199,13 +199,6 @@
|
|||||||
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import JsonTreeNode from '../components/JsonTreeNode.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 inputJson = ref('')
|
||||||
const sidebarOpen = ref(false)
|
const sidebarOpen = ref(false)
|
||||||
const leftPanelWidth = ref(50)
|
const leftPanelWidth = ref(50)
|
||||||
@@ -609,7 +602,7 @@ const handleJsonPathInput = () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
matchedPaths.value.clear()
|
matchedPaths.value.clear()
|
||||||
// JSONPath 解析错误,忽略
|
console.error('JSONPath 解析错误:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,7 +631,7 @@ const loadJsonPathHistory = () => {
|
|||||||
jsonPathHistory.value = JSON.parse(stored)
|
jsonPathHistory.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 加载 JSONPath 历史记录失败,重置为空数组
|
console.error('加载 JSONPath 历史记录失败', e)
|
||||||
jsonPathHistory.value = []
|
jsonPathHistory.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,7 +660,7 @@ const saveJsonPathHistory = (jsonPath) => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(JSONPATH_HISTORY_KEY, JSON.stringify(jsonPathHistory.value))
|
localStorage.setItem(JSONPATH_HISTORY_KEY, JSON.stringify(jsonPathHistory.value))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 保存 JSONPath 历史记录失败,忽略错误
|
console.error('保存 JSONPath 历史记录失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,28 +699,8 @@ 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 = () => {
|
const updateLineCount = () => {
|
||||||
applyInputLimit()
|
|
||||||
if (inputJson.value) {
|
if (inputJson.value) {
|
||||||
lineCount.value = inputJson.value.split('\n').length
|
lineCount.value = inputJson.value.split('\n').length
|
||||||
} else {
|
} else {
|
||||||
@@ -876,21 +849,12 @@ const getAllPaths = (obj, prefix = 'root') => {
|
|||||||
|
|
||||||
// 监听输入变化,实时更新树形结构
|
// 监听输入变化,实时更新树形结构
|
||||||
watch(inputJson, () => {
|
watch(inputJson, () => {
|
||||||
// 先应用大小限制
|
|
||||||
applyInputLimit()
|
|
||||||
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
// 使用nextTick确保DOM更新后再调整高度
|
// 使用nextTick确保DOM更新后再调整高度
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
}, 0)
|
}, 0)
|
||||||
if (inputJson.value.trim()) {
|
if (inputJson.value.trim()) {
|
||||||
// 检查大小,如果超过限制则不解析(避免性能问题)
|
|
||||||
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
|
||||||
treeLineCount.value = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(inputJson.value)
|
const parsed = JSON.parse(inputJson.value)
|
||||||
expandedNodes.value.clear()
|
expandedNodes.value.clear()
|
||||||
@@ -915,24 +879,9 @@ const formatJson = () => {
|
|||||||
showToast('请输入JSON数据')
|
showToast('请输入JSON数据')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查输入大小
|
|
||||||
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
|
||||||
showToast(`输入内容超过 5MB 限制,无法格式化`, 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(inputJson.value)
|
const parsed = JSON.parse(inputJson.value)
|
||||||
const formatted = JSON.stringify(parsed, null, 2)
|
inputJson.value = JSON.stringify(parsed, null, 2)
|
||||||
|
|
||||||
// 检查格式化后的大小
|
|
||||||
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
|
|
||||||
showToast('格式化后的内容超过 5MB 限制,无法显示', 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
inputJson.value = formatted
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('格式化成功', 'info', 2000)
|
showToast('格式化成功', 'info', 2000)
|
||||||
@@ -947,24 +896,9 @@ const minifyJson = () => {
|
|||||||
showToast('请输入JSON数据')
|
showToast('请输入JSON数据')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查输入大小
|
|
||||||
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
|
||||||
showToast(`输入内容超过 5MB 限制,无法压缩`, 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(inputJson.value)
|
const parsed = JSON.parse(inputJson.value)
|
||||||
const minified = JSON.stringify(parsed)
|
inputJson.value = JSON.stringify(parsed)
|
||||||
|
|
||||||
// 检查压缩后的大小(压缩后应该更小,但为了安全还是检查)
|
|
||||||
if (getByteLength(minified) > MAX_INPUT_BYTES) {
|
|
||||||
showToast('压缩后的内容超过 5MB 限制,无法显示', 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
inputJson.value = minified
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('压缩成功', 'info', 2000)
|
showToast('压缩成功', 'info', 2000)
|
||||||
@@ -979,13 +913,6 @@ const escapeJson = () => {
|
|||||||
showToast('请输入JSON数据')
|
showToast('请输入JSON数据')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查输入大小
|
|
||||||
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
|
||||||
showToast(`输入内容超过 5MB 限制,无法转义`, 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let jsonToEscape = inputJson.value.trim()
|
let jsonToEscape = inputJson.value.trim()
|
||||||
|
|
||||||
@@ -1039,22 +966,10 @@ const escapeJson = () => {
|
|||||||
// 不是JSON,保持原样
|
// 不是JSON,保持原样
|
||||||
}
|
}
|
||||||
// 添加引号并转义
|
// 添加引号并转义
|
||||||
const escaped = JSON.stringify(jsonToEscape)
|
inputJson.value = 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()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('转义成功', 'info', 2000)
|
showToast('转义成功', 'info', 2000)
|
||||||
@@ -1069,13 +984,6 @@ const unescapeJson = () => {
|
|||||||
showToast('请输入JSON数据')
|
showToast('请输入JSON数据')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查输入大小
|
|
||||||
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
|
||||||
showToast(`输入内容超过 5MB 限制,无法取消转义`, 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let jsonToParse = inputJson.value.trim()
|
let jsonToParse = inputJson.value.trim()
|
||||||
|
|
||||||
@@ -1119,24 +1027,13 @@ const unescapeJson = () => {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(unescaped)
|
const parsed = JSON.parse(unescaped)
|
||||||
// 如果解析成功,自动格式化
|
// 如果解析成功,自动格式化
|
||||||
const formatted = JSON.stringify(parsed, null, 2)
|
inputJson.value = JSON.stringify(parsed, null, 2)
|
||||||
// 检查格式化后的大小
|
|
||||||
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
|
|
||||||
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inputJson.value = formatted
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('取消转义并格式化成功', 'info', 2000)
|
showToast('取消转义并格式化成功', 'info', 2000)
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果解析失败,说明只是普通字符串,保持原样
|
// 如果解析失败,说明只是普通字符串,保持原样
|
||||||
// 检查字符串大小
|
|
||||||
if (getByteLength(unescaped) > MAX_INPUT_BYTES) {
|
|
||||||
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inputJson.value = unescaped
|
inputJson.value = unescaped
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
@@ -1147,25 +1044,13 @@ const unescapeJson = () => {
|
|||||||
|
|
||||||
// 如果取消转义后是对象或数组,自动格式化
|
// 如果取消转义后是对象或数组,自动格式化
|
||||||
if (typeof unescaped === 'object' && unescaped !== null) {
|
if (typeof unescaped === 'object' && unescaped !== null) {
|
||||||
const formatted = JSON.stringify(unescaped, null, 2)
|
inputJson.value = JSON.stringify(unescaped, null, 2)
|
||||||
// 检查格式化后的大小
|
|
||||||
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
|
|
||||||
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inputJson.value = formatted
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('取消转义并格式化成功', 'info', 2000)
|
showToast('取消转义并格式化成功', 'info', 2000)
|
||||||
} else {
|
} else {
|
||||||
// 其他类型(数字、布尔值等),转换为字符串
|
// 其他类型(数字、布尔值等),转换为字符串
|
||||||
const result = String(unescaped)
|
inputJson.value = String(unescaped)
|
||||||
// 检查结果大小
|
|
||||||
if (getByteLength(result) > MAX_INPUT_BYTES) {
|
|
||||||
showToast('取消转义后的内容超过 5MB 限制,无法显示', 'error', 4000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inputJson.value = result
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('取消转义成功', 'info', 2000)
|
showToast('取消转义成功', 'info', 2000)
|
||||||
@@ -1194,13 +1079,8 @@ const pasteFromClipboard = async () => {
|
|||||||
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
||||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||||
try {
|
try {
|
||||||
let text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
// 检查大小限制
|
|
||||||
if (getByteLength(text) > MAX_INPUT_BYTES) {
|
|
||||||
text = truncateToMaxBytes(text, MAX_INPUT_BYTES)
|
|
||||||
showToast('粘贴内容已超过 5MB 限制,已自动截断', 'info', 3000)
|
|
||||||
}
|
|
||||||
inputJson.value = text
|
inputJson.value = text
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
// 粘贴后不重置滚动位置,保持在当前位置
|
// 粘贴后不重置滚动位置,保持在当前位置
|
||||||
@@ -1249,16 +1129,6 @@ const handlePaste = async (event) => {
|
|||||||
const pastedText = event.clipboardData?.getData('text') || ''
|
const pastedText = event.clipboardData?.getData('text') || ''
|
||||||
|
|
||||||
if (pastedText.trim()) {
|
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已更新
|
// 等待下一个tick,确保inputJson已更新
|
||||||
await new Promise(resolve => setTimeout(resolve, 0))
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|
||||||
@@ -1293,7 +1163,7 @@ const saveToHistory = (json) => {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 读取历史记录失败,忽略错误
|
console.error('读取历史记录失败', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到开头
|
// 添加到开头
|
||||||
@@ -1309,7 +1179,7 @@ const saveToHistory = (json) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 保存历史记录失败,忽略错误
|
console.error('保存历史记录失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1321,7 +1191,7 @@ const loadHistoryList = () => {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 加载历史记录失败,重置为空数组
|
console.error('加载历史记录失败', e)
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1767,13 +1637,6 @@ onUnmounted(() => {
|
|||||||
border-bottom-color: #1a1a1a;
|
border-bottom-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-tab .size-limit {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 400;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-actions {
|
.toolbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ const saveToHistory = (text) => {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 读取历史记录失败,忽略错误
|
console.error('读取历史记录失败', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复保存相同的记录
|
// 避免重复保存相同的记录
|
||||||
@@ -243,7 +243,7 @@ const saveToHistory = (text) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 保存历史记录失败,忽略错误
|
console.error('保存历史记录失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ const loadHistoryList = () => {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 加载历史记录失败,重置为空数组
|
console.error('加载历史记录失败', e)
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
vite.config.js.timestamp-1769420606651-847d63ba5de95.mjs
Normal file
21
vite.config.js.timestamp-1769420606651-847d63ba5de95.mjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// vite.config.js
|
||||||
|
import { defineConfig } from "file:///Users/xufeng3/WebstormProjects/Toolbox/node_modules/vite/dist/node/index.js";
|
||||||
|
import vue from "file:///Users/xufeng3/WebstormProjects/Toolbox/node_modules/@vitejs/plugin-vue/dist/index.mjs";
|
||||||
|
import { resolve } from "path";
|
||||||
|
var __vite_injected_original_dirname = "/Users/xufeng3/WebstormProjects/Toolbox";
|
||||||
|
var vite_config_default = defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": resolve(__vite_injected_original_dirname, "src")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3e3,
|
||||||
|
open: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
vite_config_default as default
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMveHVmZW5nMy9XZWJzdG9ybVByb2plY3RzL1Rvb2xib3hcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy94dWZlbmczL1dlYnN0b3JtUHJvamVjdHMvVG9vbGJveC92aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMveHVmZW5nMy9XZWJzdG9ybVByb2plY3RzL1Rvb2xib3gvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgeyByZXNvbHZlIH0gZnJvbSAncGF0aCdcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgcGx1Z2luczogW3Z1ZSgpXSxcbiAgcmVzb2x2ZToge1xuICAgIGFsaWFzOiB7XG4gICAgICAnQCc6IHJlc29sdmUoX19kaXJuYW1lLCAnc3JjJylcbiAgICB9XG4gIH0sXG4gIHNlcnZlcjoge1xuICAgIHBvcnQ6IDMwMDAsXG4gICAgb3BlbjogdHJ1ZVxuICB9XG59KVxuXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQXVTLFNBQVMsb0JBQW9CO0FBQ3BVLE9BQU8sU0FBUztBQUNoQixTQUFTLGVBQWU7QUFGeEIsSUFBTSxtQ0FBbUM7QUFJekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLElBQUksQ0FBQztBQUFBLEVBQ2YsU0FBUztBQUFBLElBQ1AsT0FBTztBQUFBLE1BQ0wsS0FBSyxRQUFRLGtDQUFXLEtBQUs7QUFBQSxJQUMvQjtBQUFBLEVBQ0Y7QUFBQSxFQUNBLFFBQVE7QUFBQSxJQUNOLE1BQU07QUFBQSxJQUNOLE1BQU07QUFBQSxFQUNSO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K
|
||||||
Reference in New Issue
Block a user