diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0251301 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,104 @@ +# Dependencies +node_modules + +# Build outputs +build +dist + +# Configuration files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +jspm_packages + +# TypeScript v1 declaration files +typings + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn +.yarn-integrity +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# IDEs +.vscode +.idea +*.swp +*.swo + +# OS generated files +.DS_Store +Thumbs.db + +# Testing +coverage/ +.nyc_output/ + +# Documentation +docs/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1dba2df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Multi-stage Dockerfile for React application +# Stage 1: Build the application +FROM node:23-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Stage 2: Serve the application +FROM nginx:alpine + +# Copy built application from builder stage to nginx static directory +COPY --from=builder /app/build /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bd4383a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.8' + +services: + app: + build: . + ports: + - "80:80" + restart: unless-stopped \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..a9b075d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,52 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Log format + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Server block + server { + listen 80; + server_name localhost; + + # Serve static files directly + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + } +} \ No newline at end of file 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/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)} /> - 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: , + }, ], }, ]);