Compare commits

..

2 Commits

Author SHA1 Message Date
c6d41d18b3 init 2026-01-30 22:03:01 +08:00
6cd75e7599 init 2026-01-30 22:02:51 +08:00
6 changed files with 727 additions and 1607 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ dist-ssr
*.sw?
.cursor
/package-lock.json

1516
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,42 +5,66 @@ const routes = [
{
path: '/',
name: 'Home',
component: Home
component: Home,
meta: {
title: 'RC707的工具箱'
}
},
{
path: '/json-formatter',
name: 'JsonFormatter',
component: () => import('../views/JsonFormatter.vue')
component: () => import('../views/JsonFormatter.vue'),
meta: {
title: 'RC707的工具箱-JSON'
}
},
{
path: '/comparator',
name: 'Comparator',
component: () => import('../views/Comparator.vue')
component: () => import('../views/Comparator.vue'),
meta: {
title: 'RC707的工具箱-对比'
}
},
{
path: '/encoder-decoder',
name: 'EncoderDecoder',
component: () => import('../views/EncoderDecoder.vue')
component: () => import('../views/EncoderDecoder.vue'),
meta: {
title: 'RC707的工具箱-编解码'
}
},
{
path: '/variable-name',
name: 'VariableNameConverter',
component: () => import('../views/VariableNameConverter.vue')
component: () => import('../views/VariableNameConverter.vue'),
meta: {
title: 'RC707的工具箱-变量名'
}
},
{
path: '/qr-code',
name: 'QRCodeGenerator',
component: () => import('../views/QRCodeGenerator.vue')
component: () => import('../views/QRCodeGenerator.vue'),
meta: {
title: 'RC707的工具箱-二维码'
}
},
{
path: '/timestamp-converter',
name: 'TimestampConverter',
component: () => import('../views/TimestampConverter.vue')
component: () => import('../views/TimestampConverter.vue'),
meta: {
title: 'RC707的工具箱-时间戳'
}
},
{
path: '/color-converter',
name: 'ColorConverter',
component: () => import('../views/ColorConverter.vue')
component: () => import('../views/ColorConverter.vue'),
meta: {
title: 'RC707的工具箱-颜色'
}
}
]
@@ -49,5 +73,13 @@ const router = createRouter({
routes
})
// 路由守卫:设置页面标题
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title
}
next()
})
export default router

View File

@@ -66,6 +66,7 @@
<input
v-model.number="rgb.r"
@input="handleRgbInput"
@paste="handleRgbPaste"
type="number"
min="0"
max="255"
@@ -78,6 +79,7 @@
<input
v-model.number="rgb.g"
@input="handleRgbInput"
@paste="handleRgbPaste"
type="number"
min="0"
max="255"
@@ -90,6 +92,7 @@
<input
v-model.number="rgb.b"
@input="handleRgbInput"
@paste="handleRgbPaste"
type="number"
min="0"
max="255"
@@ -145,6 +148,7 @@
<input
v-model.number="hsl.h"
@input="handleHslInput"
@paste="handleHslPaste"
type="number"
min="0"
max="360"
@@ -157,6 +161,7 @@
<input
v-model.number="hsl.s"
@input="handleHslInput"
@paste="handleHslPaste"
type="number"
min="0"
max="100"
@@ -170,6 +175,7 @@
<input
v-model.number="hsl.l"
@input="handleHslInput"
@paste="handleHslPaste"
type="number"
min="0"
max="100"
@@ -343,6 +349,50 @@ 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输入
function handleRgbInput() {
if (updating.value) return
@@ -404,6 +454,65 @@ 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输入
function handleHslInput() {
if (updating.value) return

View File

@@ -15,8 +15,33 @@
</Transition>
<div class="main-container">
<!-- 左侧侧栏历史记录 -->
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
<div class="sidebar-header">
<h3>历史记录</h3>
<button @click="toggleSidebar" class="close-btn">×</button>
</div>
<div class="sidebar-content">
<div v-if="historyList.length === 0" class="empty-history">
暂无历史记录
</div>
<div
v-for="(item, index) in historyList"
:key="index"
class="history-item"
@click="loadHistory(item)"
>
<div class="history-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">
<div class="content-wrapper" :class="{ 'sidebar-pushed': sidebarOpen }">
<!-- 工具栏 -->
<div class="toolbar">
<div class="mode-selector">
@@ -51,6 +76,12 @@
字符维度
</button>
</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">
<button @click="performCompare" class="action-btn primary" title="开始对比">
<i class="fas fa-code-compare"></i>
@@ -80,6 +111,11 @@
</button>
</div>
</div>
<div class="sidebar-toggle">
<button @click="toggleSidebar" class="toggle-btn">
{{ sidebarOpen ? '◀' : '▶' }}
</button>
</div>
<div class="editor-container">
<div class="line-numbers">
<div v-for="n in leftLineCount" :key="n" class="line-number">{{ n }}</div>
@@ -211,7 +247,7 @@
</button>
</div>
<div class="result-fullscreen-content" v-if="compareResult">
<div class="result-panel left-result result-fullscreen-panel">
<div class="result-panel left-result result-fullscreen-panel" ref="leftFullscreenResultRef" @scroll="handleLeftFullscreenScroll">
<div class="result-line" v-for="(line, index) in compareResult.left" :key="index">
<span class="line-number">{{ line.lineNumber || index + 1 }}</span>
<span
@@ -220,7 +256,7 @@
></span>
</div>
</div>
<div class="result-panel right-result result-fullscreen-panel">
<div class="result-panel right-result result-fullscreen-panel" ref="rightFullscreenResultRef" @scroll="handleRightFullscreenScroll">
<div class="result-line" v-for="(line, index) in compareResult.right" :key="index">
<span class="line-number">{{ line.lineNumber || index + 1 }}</span>
<span
@@ -246,6 +282,7 @@ const leftText = ref('')
const rightText = ref('')
const compareMode = ref('text') // 'text' 或 'json'
const textSubMode = ref('line') // 'line' | 'char'
const ignoreListOrder = ref(false) // 是否忽略列表顺序
const leftPanelWidth = ref(50)
const rightPanelWidth = ref(50)
const isResizing = ref(false)
@@ -255,6 +292,12 @@ const leftEditorRef = ref(null)
const rightEditorRef = ref(null)
const compareResult = ref(null)
const resultFullscreen = ref(false)
const sidebarOpen = ref(false)
// 历史记录
const historyList = ref([])
const STORAGE_KEY = 'comparator-history'
const MAX_HISTORY = 50
// 全屏时禁止页面滚动
watch(resultFullscreen, (isFullscreen) => {
@@ -264,6 +307,8 @@ watch(resultFullscreen, (isFullscreen) => {
const leftResultRef = ref(null)
const rightResultRef = ref(null)
const resultContentRef = ref(null)
const leftFullscreenResultRef = ref(null)
const rightFullscreenResultRef = ref(null)
const isScrolling = ref(false)
// 提示消息
@@ -438,7 +483,7 @@ const escapeHtml = (text) => {
return div.innerHTML
}
// 同步滚动处理
// 同步滚动处理(非全屏模式)
const handleLeftScroll = () => {
if (isScrolling.value) return
isScrolling.value = true
@@ -461,6 +506,29 @@ const handleRightScroll = () => {
}, 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) => {
if (!diffRanges || diffRanges.length === 0) {
@@ -973,7 +1041,7 @@ const NodeComparisonResult = {
}
// 对比两个JSON节点返回对比结果和相似度
const compareJsonNodes = (nodeA, nodeB) => {
const compareJsonNodes = (nodeA, nodeB, ignoreOrder = false) => {
const result = (type, similarity) => ({ type, similarity, nodeA, nodeB })
if (isNullOrUndefined(nodeA) && isNullOrUndefined(nodeB)) {
@@ -992,13 +1060,16 @@ const compareJsonNodes = (nodeA, nodeB) => {
if (typeof nodeA !== typeof nodeB) {
return result(NodeComparisonResult.DIFFERENT, 0.0)
}
if (isMapType(nodeA) && isMapType(nodeB)) return compareMaps(nodeA, nodeB)
if (isListType(nodeA) && isListType(nodeB)) return compareLists(nodeA, nodeB)
if (isMapType(nodeA) && isMapType(nodeB)) return compareMaps(nodeA, nodeB, ignoreOrder)
if (isListType(nodeA) && isListType(nodeB)) {
// 根据参数选择是否忽略列表顺序
return ignoreOrder ? compareListsIgnoreOrder(nodeA, nodeB, ignoreOrder) : compareLists(nodeA, nodeB, ignoreOrder)
}
return result(NodeComparisonResult.DIFFERENT, 0.0)
}
// 对比Map对象
const compareMaps = (mapA, mapB) => {
const compareMaps = (mapA, mapB, ignoreOrder = false) => {
const keysA = Object.keys(mapA).sort()
const keysB = Object.keys(mapB).sort()
const setA = new Set(keysA)
@@ -1015,7 +1086,8 @@ const compareMaps = (mapA, mapB) => {
const hasKeyB = setB.has(key)
const valueComparison = compareJsonNodes(
hasKeyA ? mapA[key] : undefined,
hasKeyB ? mapB[key] : undefined
hasKeyB ? mapB[key] : undefined,
ignoreOrder
)
if (hasKeyA && hasKeyB && valueComparison.type === NodeComparisonResult.DIFFERENT) {
valueComparison.type = NodeComparisonResult.SIMILAR
@@ -1049,8 +1121,156 @@ const compareMaps = (mapA, mapB) => {
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数组- 寻找最佳匹配使相似度最高
const compareLists = (listA, listB) => {
const compareLists = (listA, listB, ignoreOrder = false) => {
const n = listA.length
const m = listB.length
@@ -1060,7 +1280,7 @@ const compareLists = (listA, listB) => {
for (let i = 0; i < n; i++) {
similarityMatrix[i] = []
for (let j = 0; j < m; j++) {
const comp = compareJsonNodes(listA[i], listB[j])
const comp = compareJsonNodes(listA[i], listB[j], ignoreOrder)
similarityMatrix[i][j] = comp
}
}
@@ -1070,6 +1290,15 @@ const compareLists = (listA, listB) => {
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))
// 初始化边界情况当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 j = 1; j <= m; j++) {
const matchSimilarity = similarityMatrix[i - 1][j - 1].similarity
@@ -1154,7 +1383,7 @@ const compareLists = (listA, listB) => {
}
}
if (differentMatches === totalElements) {
if (n !== 0 && m !== 0 && differentMatches === totalElements) {
return {
type: NodeComparisonResult.DIFFERENT,
similarity: 0.0,
@@ -1462,8 +1691,8 @@ const compareJson = (textA, textB) => {
const jsonA = JSON.parse(textA)
const jsonB = JSON.parse(textB)
// 执行节点对比
const comparison = compareJsonNodes(jsonA, jsonB)
// 执行节点对比,传递忽略列表顺序的参数
const comparison = compareJsonNodes(jsonA, jsonB, ignoreListOrder.value)
console.log(comparison)
// 转换为展示格式
@@ -1522,6 +1751,8 @@ const performCompare = () => {
compareResult.value = compareTextByChar(leftText.value, rightText.value)
}
}
// 保存到历史记录
saveToHistory()
showToast('对比完成', 'info', 2000)
} catch (e) {
showToast(e.message)
@@ -1555,52 +1786,16 @@ const compareTest = () => {
stats: {same: 0, insert: 0, delete: 0, modify: 0}
}
const cases = [
// 1. 完全相同的简单JSON
["{\"name\":\"John\",\"age\":30}", "{\"name\":\"John\",\"age\":30}"],
// 2. 值不同的简单JSON
["{\"name\":\"John\",\"age\":30}", "{\"name\":\"Jane\",\"age\":25}"],
// 3. 键不同
["{\"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\"]", "[]"],
["[[\"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\"}]"],
@@ -1611,8 +1806,6 @@ const compareTest = () => {
["{\"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\"}}"],
@@ -1624,7 +1817,9 @@ const compareTest = () => {
["[[[\"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',
@@ -1654,9 +1849,130 @@ const compareTest = () => {
compareResult.value = total
}
// 切换侧栏
const toggleSidebar = () => {
sidebarOpen.value = !sidebarOpen.value
}
// 格式化时间
const formatTime = (timestamp) => {
const date = new Date(timestamp)
const now = new Date()
const diff = now - date
if (diff < 60000) return '刚刚'
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 截断文本
const truncateText = (text, maxLength) => {
if (!text) 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 saveToHistory = () => {
// 如果两个输入框都为空,不保存
if (!leftText.value.trim() && !rightText.value.trim()) {
return
}
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) {
console.error('读取历史记录失败', 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) {
console.error('保存历史记录失败', e)
}
}
// 加载历史记录列表
const loadHistoryList = () => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
historyList.value = JSON.parse(stored)
}
} catch (e) {
console.error('加载历史记录失败', 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(() => {
updateLeftLineCount()
updateRightLineCount()
loadHistoryList()
// compareTest()
})
@@ -1807,12 +2123,127 @@ onUnmounted(() => {
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 {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #ffffff;
transition: margin-left 0.3s ease;
}
/* 工具栏 */
@@ -1886,6 +2317,57 @@ onUnmounted(() => {
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 {
display: flex;
gap: 0.5rem;
@@ -1940,6 +2422,34 @@ onUnmounted(() => {
flex-direction: column;
overflow: hidden;
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 {
@@ -2332,6 +2842,11 @@ onUnmounted(() => {
gap: 0.25rem;
}
.sidebar {
width: 80%;
max-width: 300px;
}
.toast-notification {
bottom: 10px;
left: 1rem;

View File

@@ -1,21 +0,0 @@
// 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