From 1cb0ed6ad250c051b1ff46f53ae2f308715684a7 Mon Sep 17 00:00:00 2001 From: vyckey Date: Sun, 24 Aug 2025 16:16:42 +0800 Subject: [PATCH 1/3] add regex tool --- src/components/CopyButton.tsx | 9 +- src/components/tools/RegexTool.tsx | 468 +++++++++++++++++++++++++++++ src/pages/ToolsLayout.tsx | 6 + src/router/Router.tsx | 5 + 4 files changed, 484 insertions(+), 4 deletions(-) create mode 100644 src/components/tools/RegexTool.tsx diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index 4ed154a..f0b52e4 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -1,10 +1,10 @@ -import { Button, message, Tooltip } from 'antd'; +import { Button, ButtonProps, message, Tooltip } from 'antd'; import React, { useState } from 'react'; import { CheckOutlined, CopyOutlined } from '@ant-design/icons'; -interface CopyButtonProps { - value: string | number | bigint | undefined; +interface CopyButtonProps extends Omit { + value: string | number | bigint | readonly string[] | undefined; } const clipboard = (window.isSecureContext && navigator.clipboard) || { @@ -25,7 +25,7 @@ const clipboard = (window.isSecureContext && navigator.clipboard) || { }), }; -const CopyButton: React.FC = ({ value }) => { +const CopyButton: React.FC = ({ value, ...props }) => { const [copied, setCopied] = useState(false); function onClick() { @@ -55,6 +55,7 @@ const CopyButton: React.FC = ({ value }) => { style={{ border: 'none' }} icon={copied ? : } onClick={onClick} + {...props} /> diff --git a/src/components/tools/RegexTool.tsx b/src/components/tools/RegexTool.tsx new file mode 100644 index 0000000..aff17a9 --- /dev/null +++ b/src/components/tools/RegexTool.tsx @@ -0,0 +1,468 @@ +import React, { useState } from 'react'; +import { Button, Flex, Input, Table, Tabs, Tag, Typography, Card, Space, Dropdown, Modal } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import type { MenuProps } from 'antd'; +import { + CaretDownFilled, + FlagFilled +} from '@ant-design/icons'; +import CopyableTextArea from '../CopyableTextArea'; + +const { Text } = Typography; + +interface MatchResult { + key: number; + index: number; + match: string; + groups: string[]; +} + +// 自定义高亮文本显示组件 +const HighlightedText: React.FC<{ text: string; pattern: string; flags: string }> = ({ text, pattern, flags }) => { + // 生成带高亮的文本 + const highlightMatches = (text: string, pattern: string, flags: string) => { + if (!pattern) return text; + + try { + // 确保全局标志存在用于高亮显示 + const effectiveFlags = flags.includes('g') ? flags : `g${flags}`; + const regex = new RegExp(`(${pattern})`, effectiveFlags); + const parts = text.split(regex); + + // 定义多种颜色用于区分不同匹配 + const highlightColors = [ + '#ffecb3', // 浅黄色 + '#b3e5fc', // 浅蓝色 + '#c8e6c9', // 浅绿色 + '#ffcdd2', // 浅红色 + '#e1bee7', // 浅紫色 + '#fff9c4', // 浅黄 + '#b2ebf2', // 浅青色 + '#d7ccc8', // 浅棕色 + ]; + + return parts.map((part, index) => { + // 如果是匹配的部分(奇数索引),则高亮显示 + if (index % 2 === 1) { + // 计算颜色索引,循环使用颜色 + const colorIndex = Math.floor(index / 2) % highlightColors.length; + return {part}; + } + return part; + }); + } catch { + // 如果正则表达式无效,返回原始文本 + return text; + } + }; + + return
{highlightMatches(text, pattern, flags)}
; +}; + +// 语法参考组件 +const SyntaxReference: React.FC = () => { + return ( +
+
+
字符匹配
+
. - 匹配除换行符以外的任意字符
+
[abc] - 匹配a, b或c的一个字符
+
[a-z] - 匹配a到z的一个字符
+
[^abc] - 匹配除a, b, c以外的一个字符
+
aa|bb - 匹配aa或bb的两个字符
+
\w - 匹配字母、数字、下划线
+
\d - 匹配数字
+
\s - 匹配空白字符
+
\W, \D, \S - 分别是上面的反义
+
+ +
+
位置匹配
+
^ - 匹配字符串开始
+
$ - 匹配字符串结束
+
\b - 匹配单词边界
+
\B - 匹配非单词边界
+
+ +
+
量词
+
* - 匹配0次或多次
+
+ - 匹配1次或多次
+
? - 匹配0次或1次
+
{'{n}'} - 匹配n次
+
{'{n,}'} - 匹配n次及以上
+
{'{m,n}'} - 匹配m到n次
+
+ +
+
分组和引用
+
(...) - 分组
+
(?:...) - 非捕获分组
+
(?=...) - 正向先行断言
+
(?!...) - 负向先行断言
+
+
+ ); +}; + +// 常用表达式下拉组件(用于放置在修饰符右侧) +const CommonExpressionsDropdown: React.FC<{ + setPattern: (pattern: string) => void; +}> = ({ setPattern }) => { + const { Text } = Typography; + + const commonPatterns: { name: string; pattern: string; description: string }[] = [ + { + name: '英文和数字', + pattern: '^[A-Za-z0-9]+$', + description: '匹配英文字符和数字的组合' + }, + { + name: '26个英文字母', + pattern: '^[A-Za-z]+$', + description: '匹配英文字母' + }, + { + name: '数字和26个英文字母', + pattern: '^[A-Za-z0-9]+$', + description: '匹配数字和英文字母的组合' + }, + { + name: '数字、字母或下划线', + pattern: '^\\w+$', + description: '匹配数字、字母或下划线' + }, + { + name: '汉字', + pattern: '^[\\u4e00-\\u9fa5]+$', + description: '匹配一个或多个汉字' + }, + { + name: 'Email地址', + pattern: '^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$', + description: '匹配标准的Email地址格式' + }, + { + name: '域名', + pattern: '[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\\.?', + description: '匹配域名格式' + }, + { + name: 'Internet URL', + pattern: '^[a-zA-Z]+://[^\\s]+$', + description: '匹配Internet URL地址' + }, + { + name: '日期格式', + pattern: '\\d{4}-\\d{2}-\\d{2}', + description: '匹配YYYY-MM-DD日期格式' + }, + { + name: '电话号码(区号-号码)', + pattern: '^(\\d{3,4}-)?\\d{7,8}$', + description: '匹配带区号的电话号码' + }, + { + name: '国内电话号码', + pattern: '\\d{3}-\\d{8}|\\d{4}-\\d{7}', + description: '匹配国内固定电话号码' + }, + { + name: '身份证号', + pattern: '(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}[\\dXx]$)', + description: '匹配15位或18位身份证号码' + }, + { + name: '中国邮政编码', + pattern: '[1-9]\\d{5}(?!\\d)', + description: '匹配中国邮政编码' + }, + { + name: 'URL链接', + pattern: 'https?://(?:[-\\w.])+(?:\\:[0-9]+)?(?:/(?:[\\w/_.])*(?:\\?(?:[\\w&=%.])*)?(?:#(?:[\\w.])*)?)?', + description: '匹配HTTP/HTTPS链接' + }, + { + name: 'IPv4地址', + pattern: '((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}', + description: '匹配IPv4地址格式' + }, + { + name: 'IP地址', + pattern: '(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)', + description: '匹配IPv4地址' + }, + ]; + + const commonPatternsMenuItems: MenuProps['items'] = commonPatterns.map((item, index) => ({ + key: index.toString(), + label: ( +
+
{item.name}
+
{item.pattern}
+
{item.description}
+
+ ), + onClick: () => setPattern(item.pattern), + })); + + return ( + + + + ); +}; + +const RegexTool: React.FC = () => { + const [pattern, setPattern] = useState(''); + const [text, setText] = useState(''); + const [replaceText, setReplaceText] = useState(''); + const [matchResults, setMatchResults] = useState([]); + const [replacedText, setReplacedText] = useState(''); + const [error, setError] = useState(''); + const [flags, setFlags] = useState('g'); // 默认全局匹配 + const [isModalVisible, setIsModalVisible] = useState(false); + + const handleMatch = () => { + setError(''); + try { + console.log('matching', pattern, flags, text) + const regex = new RegExp(pattern, flags); + const matches = []; + let match; + let index = 0; + + while ((match = regex.exec(text)) !== null) { + matches.push({ + key: index, + index: match.index, + match: match[0], + groups: match.slice(1) + }); + index++; + + // 防止无限循环 + if (match.index === regex.lastIndex) { + regex.lastIndex++; + // 如果仍然没有进展,跳出循环以防止真正的无限循环 + if (match.index === regex.lastIndex) { + break; + } + } + } + + setMatchResults(matches); + setReplacedText(text.replace(new RegExp(pattern, flags), replaceText)); + } catch (e) { + setError(e instanceof Error ? e.message : 'Invalid regular expression'); + setMatchResults([]); + setReplacedText(''); + } + }; + + const handleReplace = () => { + setError(''); + try { + const regex = new RegExp(pattern, flags); + const result = text.replace(regex, replaceText); + setReplacedText(result); + } catch (e) { + setError(e instanceof Error ? e.message : 'Invalid regular expression'); + } + }; + + const handleClear = () => { + setPattern(''); + setText(''); + setReplaceText(''); + setMatchResults([]); + setReplacedText(''); + setError(''); + }; + + const toggleFlag = (flag: string) => { + if (flags.includes(flag)) { + setFlags(flags.replace(flag, '')); + } else { + setFlags(flags + flag); + } + }; + + const matchColumns: ColumnsType = [ + { + title: '序号', + dataIndex: 'key', + key: 'key', + render: (_, __, index) => index + 1, + }, + { + title: '位置', + dataIndex: 'index', + key: 'index', + }, + { + title: '匹配内容', + dataIndex: 'match', + key: 'match', + }, + { + title: '捕获组', + dataIndex: 'groups', + key: 'groups', + render: (groups: string[]) => ( + + {groups.map((group, i) => ( + {group || '(空)'} + ))} + + ), + }, + ]; + + // 修饰符菜单项 + const flagMenuItems: MenuProps['items'] = [ + { + flag: 'g', + name: '全局匹配 (g)' + }, + { + flag: 'i', + name: '忽略大小写 (i)' + }, + { + flag: 'm', + name: '多行匹配 (m)' + }, + { + flag: 's', + name: '单行匹配 (s)' + } + ].map(({ flag, name }) => ({ + key: flag, + label: ( + + toggleFlag(flag)} + style={{ margin: 0 }} + /> + {name} + + ), + onClick: () => toggleFlag(flag), + })) as MenuProps['items']; + + return ( + <> + + setIsModalVisible(true)} style={{ padding: 0 }}>语法参考} + > + + + 正则表达式: + setPattern(e.target.value)} + addonBefore='/' + addonAfter={flags} + placeholder='输入正则表达式,如: \d+' + style={{ flex: 1 }} + /> + + + + + + + + 替换为: + setReplaceText(e.target.value)} + placeholder='输入替换文本' + style={{ flex: 1 }} + /> + + + + + + + + + {error && ( + {error} + )} + + + + +
+ +
+
+ setText(e.target.value)} + placeholder='在此输入待匹配的文本' + autoSize={{ minRows: 4, maxRows: 20 }} + showCount + /> +
+
+ + + + + ), + }, + { + key: '2', + label: '替换结果', + children: ( + + + + ), + }, + ]} + /> + + setIsModalVisible(false)} + onCancel={() => setIsModalVisible(false)} + width={600} + footer={[ + + ]} + > + + + + ); +}; + +export default RegexTool; \ No newline at end of file diff --git a/src/pages/ToolsLayout.tsx b/src/pages/ToolsLayout.tsx index b6f6911..9be93c6 100644 --- a/src/pages/ToolsLayout.tsx +++ b/src/pages/ToolsLayout.tsx @@ -3,6 +3,7 @@ import { ClockCircleOutlined, DiffOutlined, CodeOutlined, + FontSizeOutlined, LockOutlined, QrcodeOutlined, HomeOutlined, @@ -61,6 +62,11 @@ const topItems: MenuItem[] = [ label: 'Cron表达式', icon: , }, + { + key: 'regex', + label: '正则表达式', + icon: , + }, ]; const ToolsLayout: React.FC = () => { diff --git a/src/router/Router.tsx b/src/router/Router.tsx index e4284ba..72e26b5 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -12,6 +12,7 @@ import TextDiffTool from '../components/tools/TextDiffTool'; import IPAddressView from '../components/tools/IPAddressView'; import EncoderDecoderTabs from '../components/tools/EncoderDecoder'; import CronTool from '../components/tools/CronTool'; +import RegexTool from '../components/tools/RegexTool'; const router = createBrowserRouter([ { @@ -66,6 +67,10 @@ const router = createBrowserRouter([ path: 'cron', element: , }, + { + path: 'regex', + element: , + }, ], }, ]); From 85dba4cf0343c35db92b8a4d57ec4c1ead2c7eb7 Mon Sep 17 00:00:00 2001 From: vyckey Date: Sun, 24 Aug 2025 16:37:44 +0800 Subject: [PATCH 2/3] opt json tool --- src/components/tools/JsonTools.tsx | 59 ++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/components/tools/JsonTools.tsx b/src/components/tools/JsonTools.tsx index 63314b2..2ec0afe 100644 --- a/src/components/tools/JsonTools.tsx +++ b/src/components/tools/JsonTools.tsx @@ -2,6 +2,10 @@ import React, { useState, useRef, useEffect } from 'react'; import { Button, Flex, Input, message, Select, Switch, Tabs, InputRef } from 'antd'; import { Splitter } from 'antd'; import ReactJson, { InteractionProps, OnSelectProps, ThemeKeys } from 'react-json-view'; +import { + DownloadOutlined, + SortAscendingOutlined, +} from '@ant-design/icons'; interface JsonViewerProps { initialData?: object; @@ -64,15 +68,15 @@ const JsonViewer: React.FC = ({ initialData }) => { useEffect(() => { if (!initialData) { const sampleData = { - name: "Vyckey", - email: "vyckey@qq.com", - avatar: "https://vyckey.github.io/avatar.jpg", - website: "https://vyckey.github.io", + name: 'Vyckey', + email: 'vyckey@qq.com', + avatar: 'https://vyckey.github.io/avatar.jpg', + website: 'https://vyckey.github.io', address: { - city: "Shanghai", - country: "China" + city: 'Shanghai', + country: 'China' }, - hobbies: ["reading", "coding"], + hobbies: ['reading', 'coding'], isActive: true }; setJsonValue(sampleData); @@ -217,9 +221,29 @@ const JsonViewer: React.FC = ({ initialData }) => { // Handle selection if needed }; + const handleExport = () => { + try { + const obj = JSON.parse(textValue); + const blob = new Blob([JSON.stringify(obj, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'data.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : String(e); + message.error(`JSON 格式错误: ${errorMessage}`); + } + }; + return ( <> - + = ({ initialData }) => { }))} onChange={(val: number) => setIndentWidth(val)} /> -