Skip to content

Commit e636865

Browse files
bradbrad
authored andcommitted
fix bug
1 parent 8603dcf commit e636865

10 files changed

Lines changed: 2463 additions & 1110 deletions

File tree

pnpm-lock.yaml

Lines changed: 1108 additions & 973 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/icons/128.png

-9.32 KB
Binary file not shown.

src/popup/components/Editor.tsx

Lines changed: 382 additions & 21 deletions
Large diffs are not rendered by default.

src/popup/components/JsonViewer.tsx

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,162 @@
1-
import React from 'react';
1+
import React, { useRef, useEffect } from 'react';
22
import JsonView, { JsonViewProps } from 'react18-json-view';
33
import 'react18-json-view/src/style.css';
44

55
interface JsonViewerProps {
66
data: any;
77
theme: 'light' | 'dark';
8+
searchQuery?: string;
9+
matchCase?: boolean;
810
}
911

10-
const JsonViewer: React.FC<JsonViewerProps> = ({ data, theme }) => {
12+
const JsonViewer: React.FC<JsonViewerProps> = ({ data, theme, searchQuery = '', matchCase = false }) => {
13+
const containerRef = useRef<HTMLDivElement>(null);
14+
const highlightTimeoutRef = useRef<NodeJS.Timeout | null>(null);
15+
const isHighlightingRef = useRef(false);
16+
17+
// 高亮搜索结果
18+
useEffect(() => {
19+
const container = containerRef.current;
20+
if (!container) return;
21+
22+
// 清除之前的定时器
23+
if (highlightTimeoutRef.current) {
24+
clearTimeout(highlightTimeoutRef.current);
25+
}
26+
27+
const performHighlight = () => {
28+
if (isHighlightingRef.current) return;
29+
isHighlightingRef.current = true;
30+
31+
try {
32+
console.log('[JsonViewer] Starting highlight with query:', searchQuery);
33+
34+
// 移除所有旧的高亮
35+
const oldHighlights = container.querySelectorAll('mark.json-search-highlight');
36+
console.log('[JsonViewer] Removing old highlights:', oldHighlights.length);
37+
oldHighlights.forEach(mark => {
38+
const parent = mark.parentNode;
39+
if (parent) {
40+
const text = mark.textContent || '';
41+
parent.replaceChild(document.createTextNode(text), mark);
42+
}
43+
});
44+
45+
// 规范化文本节点
46+
container.normalize();
47+
48+
if (!searchQuery.trim()) {
49+
console.log('[JsonViewer] Empty search query, skipping highlight');
50+
isHighlightingRef.current = false;
51+
return;
52+
}
53+
54+
// 创建正则表达式
55+
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56+
console.log('[JsonViewer] Escaped query:', escapedQuery);
57+
58+
// 获取所有文本节点
59+
const getTextNodes = (node: Node): Text[] => {
60+
const textNodes: Text[] = [];
61+
62+
const walk = (currentNode: Node) => {
63+
if (currentNode.nodeType === Node.TEXT_NODE) {
64+
const text = currentNode.textContent || '';
65+
if (text.trim()) {
66+
textNodes.push(currentNode as Text);
67+
}
68+
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
69+
// 跳过已经是高亮标记的元素
70+
if ((currentNode as Element).tagName === 'MARK') {
71+
return;
72+
}
73+
currentNode.childNodes.forEach(walk);
74+
}
75+
};
76+
77+
walk(node);
78+
return textNodes;
79+
};
80+
81+
const textNodes = getTextNodes(container);
82+
console.log('[JsonViewer] Found text nodes:', textNodes.length);
83+
84+
let totalMatches = 0;
85+
// 对每个文本节点进行高亮
86+
textNodes.forEach(textNode => {
87+
const text = textNode.textContent || '';
88+
const matches: Array<{ index: number; text: string }> = [];
89+
90+
let match;
91+
const testRegex = new RegExp(escapedQuery, matchCase ? 'g' : 'gi');
92+
while ((match = testRegex.exec(text)) !== null) {
93+
matches.push({
94+
index: match.index,
95+
text: match[0]
96+
});
97+
}
98+
99+
if (matches.length > 0) {
100+
totalMatches += matches.length;
101+
const fragment = document.createDocumentFragment();
102+
let lastIndex = 0;
103+
104+
matches.forEach(({ index, text: matchText }) => {
105+
// 添加匹配前的文本
106+
if (index > lastIndex) {
107+
fragment.appendChild(
108+
document.createTextNode(text.substring(lastIndex, index))
109+
);
110+
}
111+
112+
// 添加高亮标记
113+
const mark = document.createElement('mark');
114+
mark.className = 'json-search-highlight';
115+
mark.textContent = matchText;
116+
mark.style.cssText = 'display: inline !important; visibility: visible !important;';
117+
fragment.appendChild(mark);
118+
119+
lastIndex = index + matchText.length;
120+
});
121+
122+
// 添加剩余文本
123+
if (lastIndex < text.length) {
124+
fragment.appendChild(
125+
document.createTextNode(text.substring(lastIndex))
126+
);
127+
}
128+
129+
// 替换原文本节点
130+
const parent = textNode.parentNode;
131+
if (parent) {
132+
parent.replaceChild(fragment, textNode);
133+
}
134+
}
135+
});
136+
137+
console.log('[JsonViewer] Highlighted', totalMatches, 'matches');
138+
} catch (error) {
139+
console.error('[JsonViewer] Highlight error:', error);
140+
} finally {
141+
isHighlightingRef.current = false;
142+
}
143+
};
144+
145+
// 延迟执行高亮,等待 JsonView 完全渲染
146+
highlightTimeoutRef.current = setTimeout(performHighlight, 200);
147+
148+
return () => {
149+
if (highlightTimeoutRef.current) {
150+
clearTimeout(highlightTimeoutRef.current);
151+
}
152+
};
153+
}, [searchQuery, matchCase, data]);
11154
const handleEdit: JsonViewProps['onEdit'] = () => {};
12155
const handleAdd: JsonViewProps['onAdd'] = () => {};
13156
const handleDelete: JsonViewProps['onDelete'] = () => {};
14157

15158
return (
16-
<div className="json-viewer-container">
159+
<div className="json-viewer-container" ref={containerRef}>
17160
<JsonView
18161
src={data}
19162
theme={theme === 'dark' ? 'a11y' : 'default'}

src/popup/components/SearchBar.tsx

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { FiSearch, FiX, FiChevronUp, FiChevronDown } from 'react-icons/fi';
3+
4+
interface SearchBarProps {
5+
onSearch: (query: string, matchCase: boolean) => void;
6+
onClose: () => void;
7+
currentMatch?: number;
8+
totalMatches?: number;
9+
onNext: () => void;
10+
onPrevious: () => void;
11+
}
12+
13+
const SearchBar: React.FC<SearchBarProps> = ({
14+
onSearch,
15+
onClose,
16+
currentMatch = 0,
17+
totalMatches = 0,
18+
onNext,
19+
onPrevious
20+
}) => {
21+
const [searchQuery, setSearchQuery] = useState('');
22+
const [matchCase, setMatchCase] = useState(false);
23+
const inputRef = useRef<HTMLInputElement>(null);
24+
25+
useEffect(() => {
26+
// 自动聚焦输入框
27+
const timer = setTimeout(() => {
28+
inputRef.current?.focus();
29+
inputRef.current?.select();
30+
}, 0);
31+
return () => clearTimeout(timer);
32+
}, []);
33+
34+
// 确保焦点始终在搜索框(仅在失去焦点到搜索栏外部时重新聚焦)
35+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
36+
// 检查新的焦点是否在搜索栏内部
37+
const relatedTarget = e.relatedTarget as HTMLElement;
38+
const searchBar = e.currentTarget.closest('.search-bar');
39+
40+
// 如果焦点移动到搜索栏外部,重新聚焦输入框
41+
if (relatedTarget && (!searchBar || !searchBar.contains(relatedTarget))) {
42+
setTimeout(() => {
43+
const activeElement = document.activeElement as HTMLElement;
44+
if (!searchBar || !searchBar.contains(activeElement)) {
45+
inputRef.current?.focus();
46+
}
47+
}, 0);
48+
}
49+
};
50+
51+
useEffect(() => {
52+
// 当查询变化时触发搜索
53+
if (searchQuery) {
54+
onSearch(searchQuery, matchCase);
55+
}
56+
}, [searchQuery, matchCase, onSearch]);
57+
58+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
59+
// 阻止事件冒泡,防止被编辑器捕获
60+
e.stopPropagation();
61+
62+
if (e.key === 'Escape') {
63+
e.preventDefault();
64+
onClose();
65+
} else if (e.key === 'Enter') {
66+
e.preventDefault();
67+
if (e.shiftKey) {
68+
onPrevious();
69+
} else {
70+
onNext();
71+
}
72+
} else if (e.key === 'F3') {
73+
e.preventDefault();
74+
if (e.shiftKey) {
75+
onPrevious();
76+
} else {
77+
onNext();
78+
}
79+
}
80+
// 如果按下 Ctrl+F 或 Cmd+F,关闭搜索栏(防止重复打开)
81+
else if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
82+
e.preventDefault();
83+
onClose();
84+
}
85+
};
86+
87+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
88+
setSearchQuery(e.target.value);
89+
};
90+
91+
return (
92+
<div className="search-bar">
93+
<div className="search-bar-content">
94+
<FiSearch className="search-icon" size={18} />
95+
<input
96+
ref={inputRef}
97+
type="text"
98+
className="search-input"
99+
placeholder="查找..."
100+
value={searchQuery}
101+
onChange={handleInputChange}
102+
onKeyDown={handleKeyDown}
103+
onBlur={handleBlur}
104+
autoFocus
105+
onMouseDown={(e) => {
106+
// 确保点击输入框时不会失去焦点
107+
e.stopPropagation();
108+
}}
109+
/>
110+
<div className="search-controls">
111+
<button
112+
className="search-button"
113+
onClick={onPrevious}
114+
disabled={totalMatches === 0}
115+
title="上一个 (Shift+Enter)"
116+
>
117+
<FiChevronUp size={18} />
118+
</button>
119+
<button
120+
className="search-button"
121+
onClick={onNext}
122+
disabled={totalMatches === 0}
123+
title="下一个 (Enter)"
124+
>
125+
<FiChevronDown size={18} />
126+
</button>
127+
<label className="search-option">
128+
<input
129+
type="checkbox"
130+
checked={matchCase}
131+
onChange={(e) => setMatchCase(e.target.checked)}
132+
/>
133+
<span>区分大小写</span>
134+
</label>
135+
{totalMatches > 0 && (
136+
<span className="search-results">
137+
{currentMatch} / {totalMatches}
138+
</span>
139+
)}
140+
<button
141+
className="search-close"
142+
onClick={onClose}
143+
title="关闭 (Esc)"
144+
>
145+
<FiX size={18} />
146+
</button>
147+
</div>
148+
</div>
149+
</div>
150+
);
151+
};
152+
153+
export default SearchBar;

src/popup/components/Sidebar.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { FiSun, FiMoon, FiCopy, FiTrash2, FiCode, FiMinimize2, FiRefreshCw, FiClock } from 'react-icons/fi';
2+
import { FiSun, FiMoon, FiCopy, FiTrash2, FiCode, FiMinimize2, FiRefreshCw, FiClock, FiArrowRight, FiArrowLeft } from 'react-icons/fi';
33
import Tooltip from './Tooltip';
44

55
interface SidebarProps {
@@ -8,6 +8,8 @@ interface SidebarProps {
88
onFormat: () => void;
99
onMinify: () => void;
1010
onStringify: () => void;
11+
onJsonToString: () => void;
12+
onStringToJson: () => void;
1113
onCopy: () => void;
1214
onClear: () => void;
1315
onHistoryClick: () => void;
@@ -21,6 +23,8 @@ const Sidebar: React.FC<SidebarProps> = ({
2123
onFormat,
2224
onMinify,
2325
onStringify,
26+
onJsonToString,
27+
onStringToJson,
2428
onCopy,
2529
onClear,
2630
onHistoryClick,
@@ -69,6 +73,26 @@ const Sidebar: React.FC<SidebarProps> = ({
6973
<FiRefreshCw size={20} />
7074
</button>
7175
</Tooltip>
76+
77+
<Tooltip text="JSON → JSON String" position="right">
78+
<button
79+
className="sidebar-button"
80+
onClick={onJsonToString}
81+
disabled={!hasContent}
82+
>
83+
<FiArrowRight size={20} />
84+
</button>
85+
</Tooltip>
86+
87+
<Tooltip text="JSON String → JSON" position="right">
88+
<button
89+
className="sidebar-button"
90+
onClick={onStringToJson}
91+
disabled={!hasContent}
92+
>
93+
<FiArrowLeft size={20} />
94+
</button>
95+
</Tooltip>
7296

7397
<Tooltip text="Copy to Clipboard" position="right">
7498
<button

0 commit comments

Comments
 (0)