From 6cd75e759953d69bc1e21a5eca8849eac60d4c97 Mon Sep 17 00:00:00 2001 From: rose_cat707 Date: Fri, 30 Jan 2026 22:02:51 +0800 Subject: [PATCH] init --- .gitignore | 1 + src/router/index.js | 48 ++- src/views/ColorConverter.vue | 109 ++++++ src/views/Comparator.vue | 639 +++++++++++++++++++++++++++++++---- 4 files changed, 727 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index 1e2d60c..c077a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.sw? .cursor +/package-lock.json \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 4432291..0ac184d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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 diff --git a/src/views/ColorConverter.vue b/src/views/ColorConverter.vue index 999037c..97ed1b5 100644 --- a/src/views/ColorConverter.vue +++ b/src/views/ColorConverter.vue @@ -66,6 +66,7 @@ = 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 diff --git a/src/views/Comparator.vue b/src/views/Comparator.vue index da6993e..2b1ffd7 100644 --- a/src/views/Comparator.vue +++ b/src/views/Comparator.vue @@ -15,8 +15,33 @@
+ + + -
+
@@ -51,6 +76,12 @@ 字符维度
+
+ +
+
{{ n }}
@@ -211,7 +247,7 @@
-
+
{{ line.lineNumber || index + 1 }}
-
+
{{ line.lineNumber || index + 1 }} { @@ -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;