├── .nvmrc
├── app
├── favicon.ico
├── robots.ts
├── ai
│ └── page.tsx
├── image
│ └── page.tsx
├── globals.css
├── sitemap.ts
├── utils
│ ├── tryParseJson.ts
│ └── fetchToCurl.ts
├── components
│ ├── FriendLinks.tsx
│ ├── Navigation.tsx
│ ├── GitHubLink.tsx
│ ├── JsonLd.tsx
│ ├── KeywordOptimization.tsx
│ ├── RelatedTools.tsx
│ ├── PerformanceMonitor.tsx
│ ├── QuickTip.tsx
│ ├── JsonParser.tsx
│ ├── VisitorStats.tsx
│ └── FetchToCurl.tsx
├── api
│ ├── video
│ │ └── convert
│ │ │ └── route.ts
│ ├── convert
│ │ └── route.ts
│ └── og
│ │ └── route.tsx
├── metadata.ts
├── layout.tsx
├── video
│ └── page.tsx
└── page.tsx
├── husky
└── pre-commit
├── public
├── vercel.svg
├── sitemap.xml
├── baidusitemap.xml
├── robots.txt
├── window.svg
├── file.svg
├── globe.svg
├── sitemap-0.xml
└── next.svg
├── dv.md
├── urls.txt
├── postcss.config.mjs
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── ecosystem.config.js
├── eslint.config.mjs
├── tailwind.config.ts
├── .gitignore
├── tsconfig.json
├── next-sitemap.config.js
├── next.config.ts
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/text-escape/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run version-auto
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dv.md:
--------------------------------------------------------------------------------
1 | curl -H 'Content-Type:text/plain' --data-binary @urls.txt "http://data.zz.baidu.com/urls?site=https://text-escape.jcommon.top&token=Ce9lijsmmj0KydIb"
--------------------------------------------------------------------------------
/urls.txt:
--------------------------------------------------------------------------------
1 | https://text-escape.jcommon.top
2 | https://text-escape.jcommon.top/api/og
3 | https://text-escape.jcommon.top/en
4 | https://text-escape.jcommon.top/video
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "esbenp.prettier-vscode",
5 | "stylelint.vscode-stylelint",
6 | "visualstudioexptteam.vscodeintellicode"
7 | ]
8 | }
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://text-escape.jcommon.top/sitemap-0.xml
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // 使用 IntelliSense 了解相关属性。
3 | // 悬停以查看现有属性的描述。
4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 |
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/public/baidusitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://text-escape.jcommon.top
5 | 2024-03-21
6 | daily
7 | 1.0
8 |
9 |
--------------------------------------------------------------------------------
/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next';
2 |
3 | export default function robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: '*',
7 | allow: '/',
8 | disallow: ['/api/*'],
9 | },
10 | sitemap: 'https://text-escape.jcommon.top/sitemap.xml',
11 | };
12 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # *
2 | User-agent: *
3 | Allow: /
4 | Disallow: /api/
5 | Disallow: /admin/
6 |
7 | # Baiduspider
8 | User-agent: Baiduspider
9 | Allow: /
10 | Crawl-delay: 1
11 |
12 | # Host
13 | Host: https://text-escape.jcommon.top
14 |
15 | # Sitemaps
16 | Sitemap: https://text-escape.jcommon.top/sitemap.xml
17 |
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [{
3 | name: 'text-escape',
4 | script: 'node_modules/next/dist/bin/next',
5 | args: 'start',
6 | instances: 1,
7 | autorestart: true,
8 | watch: false,
9 | max_memory_restart: '1G',
10 | env: {
11 | NODE_ENV: 'production',
12 | PORT: process.env.PORT || 3005
13 | }
14 | }]
15 | }
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/ai/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function AiPage() {
4 | return (
5 |
6 |
AI 工具
7 |
8 | 这里将会集成各种实用的 AI 工具,敬请期待!
9 |
10 | {/* 你可以在这里添加更多 AI 相关的功能和内容 */}
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/app/image/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function ImagePage() {
4 | return (
5 |
6 |
图片工具
7 |
8 | 这里将会集成各种实用的图片处理工具,敬请期待!
9 |
10 | {/* 你可以在这里添加更多图片相关的功能和内容 */}
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #ffffff;
7 | --foreground: #171717;
8 | }
9 |
10 | @media (prefers-color-scheme: dark) {
11 | :root {
12 | --background: #0a0a0a;
13 | --foreground: #ededed;
14 | }
15 | }
16 |
17 | body {
18 | color: var(--foreground);
19 | background: var(--background);
20 | font-family: Arial, Helvetica, sans-serif;
21 | }
22 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: "var(--background)",
13 | foreground: "var(--foreground)",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | } satisfies Config;
19 |
--------------------------------------------------------------------------------
/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next';
2 |
3 | export default function sitemap(): MetadataRoute.Sitemap {
4 | const baseUrl = 'https://text-escape.jcommon.top';
5 |
6 | return [
7 | {
8 | url: baseUrl,
9 | lastModified: new Date(),
10 | changeFrequency: 'daily',
11 | priority: 1,
12 | },
13 | {
14 | url: `${baseUrl}/video`,
15 | lastModified: new Date(),
16 | changeFrequency: 'weekly',
17 | priority: 0.8,
18 | },
19 | ];
20 | }
--------------------------------------------------------------------------------
/app/utils/tryParseJson.ts:
--------------------------------------------------------------------------------
1 | export function tryParseJson(str: string): string {
2 | let result = str;
3 | try {
4 | result = JSON.parse(str);
5 | } catch {
6 | try {
7 | const start = str.indexOf("```json\n");
8 | const end = str.lastIndexOf("\n```");
9 | if (start !== -1 && end !== -1) {
10 | result = JSON.parse(str.substring(start + 8, end));
11 | }
12 | } catch (err) {
13 | console.warn("tryParseJson error", err);
14 | }
15 | }
16 | return typeof result === "string" ? result : JSON.stringify(result, null, 2);
17 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
43 | yarn.lock
44 |
45 | package-lock.json
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/app/components/FriendLinks.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export default function FriendLinks() {
4 | const links = [
5 | {
6 | name: 'Markdown转图片工具',
7 | url: 'https://markdown-to-image-serve.jcommon.top',
8 | icon: '📝'
9 | },
10 | // 可以在这里添加更多友情链接
11 | ]
12 |
13 | return (
14 |
29 | )
30 | }
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next-sitemap').IConfig} */
2 | module.exports = {
3 | siteUrl: 'https://text-escape.jcommon.top',
4 | generateRobotsTxt: true,
5 | changefreq: 'daily',
6 | priority: 1.0,
7 | sitemapSize: 7000,
8 |
9 | transform: async (config, path) => {
10 | // 根据路径自定义优先级
11 | const priority = path === '/' ? 1.0 : 0.8
12 | const changefreq = path === '/' ? 'daily' : 'weekly'
13 |
14 | return {
15 | loc: path,
16 | changefreq,
17 | priority,
18 | lastmod: new Date().toISOString(),
19 | }
20 | },
21 |
22 | robotsTxtOptions: {
23 | policies: [
24 | {
25 | userAgent: '*',
26 | allow: '/',
27 | disallow: ['/api/', '/admin/'],
28 | },
29 | // 针对百度爬虫的特殊规则
30 | {
31 | userAgent: 'Baiduspider',
32 | allow: '/',
33 | crawlDelay: 1,
34 | }
35 | ],
36 | },
37 | }
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/sitemap-0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://text-escape.jcommon.top/robots.txt2025-05-09T03:09:32.481Zweekly0.8
4 | https://text-escape.jcommon.top2025-05-09T03:09:32.482Zdaily1
5 | https://text-escape.jcommon.top/sitemap.xml2025-05-09T03:09:32.482Zweekly0.8
6 | https://text-escape.jcommon.top/video2025-05-09T03:09:32.482Zweekly0.8
7 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 | // import { withContentlayer } from 'next-contentlayer'
3 |
4 | const nextConfig: NextConfig = {
5 | /* config options here */
6 | headers: async () => {
7 | return [
8 | {
9 | source: '/:path*',
10 | headers: [
11 | {
12 | key: 'X-Robots-Tag',
13 | value: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
14 | },
15 | {
16 | key: 'X-Content-Type-Options',
17 | value: 'nosniff',
18 | },
19 | {
20 | key: 'X-Frame-Options',
21 | value: 'DENY',
22 | },
23 | {
24 | key: 'X-XSS-Protection',
25 | value: '1; mode=block',
26 | }
27 | ],
28 | },
29 | ]
30 | },
31 | poweredByHeader: false,
32 | compress: true,
33 | images: {
34 | domains: ['text-escape.jcommon.top'],
35 | formats: ['image/avif', 'image/webp'],
36 | deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
37 | },
38 | };
39 |
40 | export default nextConfig
41 |
--------------------------------------------------------------------------------
/app/components/Navigation.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Link from 'next/link'
4 | import { usePathname } from 'next/navigation'
5 |
6 | export default function Navigation() {
7 | const pathname = usePathname()
8 |
9 | const navItems = [
10 | { name: '文本工具', path: '/' },
11 | { name: 'AI 工具', path: '/ai' },
12 | { name: '图片工具', path: '/image' },
13 | { name: '视频工具', path: '/video' },
14 | ]
15 |
16 | return (
17 |
36 | )
37 | }
--------------------------------------------------------------------------------
/app/components/GitHubLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | export default function GitHubLink() {
4 | return (
5 |
12 |
23 |
24 | );
25 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text-escape",
3 | "version": "1.1.3",
4 | "private": true,
5 | "description": "一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义",
6 | "author": "wxingheng",
7 | "scripts": {
8 | "dev": "next dev --turbopack -p 3004",
9 | "build": "next build",
10 | "start": "next start -p ${PORT:-3005}",
11 | "lint": "next lint",
12 | "postbuild": "next-sitemap",
13 | "version-auto": "jcommon-node -va -t",
14 | "pm2:start": "pm2 start ecosystem.config.js",
15 | "pm2:stop": "pm2 stop text-escape",
16 | "pm2:restart": "pm2 restart text-escape",
17 | "pm2:logs": "pm2 logs text-escape",
18 | "pm2:status": "pm2 status"
19 | },
20 | "dependencies": {
21 | "next": "15.1.2",
22 | "react": "^19.0.0",
23 | "react-dom": "^19.0.0",
24 | "tailwindcss": "^3.4.1"
25 | },
26 | "devDependencies": {
27 | "jcommon-node": "^1.0.14",
28 | "@eslint/eslintrc": "^3",
29 | "@types/node": "^20",
30 | "@types/react": "^19",
31 | "@types/react-dom": "^19",
32 | "eslint": "^9",
33 | "eslint-config-next": "15.1.2",
34 | "next-sitemap": "^4.2.3",
35 | "postcss": "^8",
36 | "typescript": "^5"
37 | },
38 | "engines": {
39 | "node": "20"
40 | },
41 | "time": "250401202509"
42 | }
43 |
--------------------------------------------------------------------------------
/app/utils/fetchToCurl.ts:
--------------------------------------------------------------------------------
1 | interface RequestOptions extends RequestInit {
2 | url: string;
3 | }
4 |
5 | export function fetchToCurl(options: RequestOptions): string {
6 | const {
7 | method = 'GET',
8 | headers = {},
9 | body,
10 | url,
11 | } = options;
12 |
13 | // 开始构建 curl 命令
14 | let curlCommand = `curl '${url}'`;
15 |
16 | // 添加请求方法
17 | if (method !== 'GET') {
18 | curlCommand += ` -X ${method}`;
19 | }
20 |
21 | // 添加请求头
22 | Object.entries(headers).forEach(([key, value]) => {
23 | curlCommand += ` \\\n -H '${key}: ${value}'`;
24 | });
25 |
26 | // 添加请求体
27 | if (body) {
28 | let bodyStr = '';
29 | if (typeof body === 'string') {
30 | bodyStr = body;
31 | } else if (body instanceof URLSearchParams) {
32 | bodyStr = body.toString();
33 | } else if (body instanceof FormData) {
34 | const formDataObj: Record = {};
35 | body.forEach((value, key) => {
36 | formDataObj[key] = value.toString();
37 | });
38 | bodyStr = JSON.stringify(formDataObj);
39 | } else if (body instanceof Blob) {
40 | bodyStr = '[Blob data]';
41 | } else {
42 | bodyStr = JSON.stringify(body);
43 | }
44 | curlCommand += ` \\\n -d '${bodyStr}'`;
45 | }
46 |
47 | return curlCommand;
48 | }
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/components/JsonLd.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | // import { usePathname } from 'next/navigation'; // 移除未使用的导入
4 |
5 | export default function JsonLd() {
6 | // const pathname = usePathname(); // 移除未使用变量
7 |
8 | const jsonLd = {
9 | '@context': 'https://schema.org',
10 | '@type': 'WebApplication',
11 | name: '文本转义工具',
12 | description: '一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义,提供实时预览和一键复制功能。',
13 | url: 'https://text-escape.jcommon.top',
14 | applicationCategory: 'DeveloperApplication',
15 | operatingSystem: 'Any',
16 | browserRequirements: 'Requires JavaScript. Requires HTML5.',
17 | offers: {
18 | '@type': 'Offer',
19 | price: '0',
20 | priceCurrency: 'CNY'
21 | },
22 | author: {
23 | '@type': 'Person',
24 | name: 'jcommon',
25 | url: 'https://github.com/wxingheng'
26 | },
27 | publisher: {
28 | '@type': 'Organization',
29 | name: 'jcommon',
30 | url: 'https://github.com/wxingheng'
31 | },
32 | mainEntityOfPage: {
33 | '@type': 'WebPage',
34 | '@id': 'https://text-escape.jcommon.top'
35 | },
36 | potentialAction: {
37 | '@type': 'UseAction',
38 | target: 'https://text-escape.jcommon.top'
39 | }
40 | };
41 |
42 | return (
43 |
47 | );
48 | }
--------------------------------------------------------------------------------
/app/components/KeywordOptimization.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | const keywords = [
4 | {
5 | category: '文本处理',
6 | terms: [
7 | '文本转义', '字符串转义', '文本反转义', '字符串反转义',
8 | '换行符转义', '双引号转义', '特殊字符转义', 'HTML转义'
9 | ]
10 | },
11 | {
12 | category: 'JSON相关',
13 | terms: [
14 | 'JSON转义', 'JSON字符串', 'JSON格式化', 'JSON压缩',
15 | 'JSON验证', 'JSON解析', 'JSON美化', 'JSON在线工具'
16 | ]
17 | },
18 | {
19 | category: '开发工具',
20 | terms: [
21 | '在线工具', '开发者工具', '编程工具', '代码工具',
22 | '文本工具', '字符串工具', '格式转换', '在线转换'
23 | ]
24 | }
25 | ];
26 |
27 | export default function KeywordOptimization() {
28 | return (
29 |
30 |
相关关键词
31 |
32 | {keywords.map((group) => (
33 |
34 |
{group.category}
35 |
36 | {group.terms.map((term) => (
37 |
41 | {term}
42 |
43 | ))}
44 |
45 |
46 | ))}
47 |
48 |
49 | );
50 | }
--------------------------------------------------------------------------------
/app/components/RelatedTools.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 |
5 | const tools = [
6 | {
7 | name: 'JSON 格式化工具',
8 | description: '在线 JSON 格式化、压缩、验证工具',
9 | url: 'https://json.jcommon.top',
10 | icon: '📝'
11 | },
12 | {
13 | name: 'Markdown 转图片',
14 | description: '将 Markdown 文档转换为图片',
15 | url: 'https://markdown-to-image-serve.jcommon.top',
16 | icon: '🖼️'
17 | },
18 | {
19 | name: '视频格式转换',
20 | description: '在线视频格式转换工具',
21 | url: 'https://video-format-convert.jcommon.top',
22 | icon: '🎥'
23 | }
24 | ];
25 |
26 | export default function RelatedTools() {
27 | return (
28 |
29 |
相关工具
30 |
31 | {tools.map((tool) => (
32 |
38 |
39 |
{tool.icon}
40 |
41 |
{tool.name}
42 |
{tool.description}
43 |
44 |
45 |
46 | ))}
47 |
48 |
49 | );
50 | }
--------------------------------------------------------------------------------
/app/api/video/convert/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server'
2 | import { writeFile, readFile, unlink } from 'fs/promises'
3 | import { join } from 'path'
4 | import { exec } from 'child_process'
5 | import { promisify } from 'util'
6 |
7 | const execAsync = promisify(exec)
8 |
9 | export async function POST(request: NextRequest) {
10 | try {
11 | const formData = await request.formData()
12 | const file = formData.get('file') as File
13 | const outputFormat = formData.get('format') as string
14 |
15 | if (!file) {
16 | return NextResponse.json(
17 | { error: '未找到文件' },
18 | { status: 400 }
19 | )
20 | }
21 |
22 | // 创建临时文件
23 | const bytes = await file.arrayBuffer()
24 | const buffer = Buffer.from(bytes)
25 | const tempInputPath = join('/tmp', file.name)
26 | const tempOutputPath = join('/tmp', `converted_${Date.now()}.${outputFormat}`)
27 |
28 | await writeFile(tempInputPath, buffer)
29 |
30 | // 使用 ffmpeg 进行转换
31 | try {
32 | await execAsync(`ffmpeg -i ${tempInputPath} ${tempOutputPath}`)
33 |
34 | // 读取转换后的文件
35 | const convertedFile = await readFile(tempOutputPath)
36 |
37 | // 清理临时文件
38 | await unlink(tempInputPath)
39 | await unlink(tempOutputPath)
40 |
41 | return new NextResponse(convertedFile, {
42 | headers: {
43 | 'Content-Type': `video/${outputFormat}`,
44 | 'Content-Disposition': `attachment; filename="converted.${outputFormat}"`,
45 | },
46 | })
47 | } catch (error) {
48 | console.error('转换失败:', error)
49 | return NextResponse.json(
50 | { error: '视频转换失败' },
51 | { status: 500 }
52 | )
53 | }
54 | } catch (error) {
55 | console.error('处理请求失败:', error)
56 | return NextResponse.json(
57 | { error: '服务器内部错误' },
58 | { status: 500 }
59 | )
60 | }
61 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // "editor.codeActionsOnSave": {
3 | // "source.fixAll.eslint": "explicit"
4 | // },
5 | "i18n-ally.localesPaths": ["src/i18n", "src/locales"],
6 | "[typescriptreact]": {
7 | "editor.defaultFormatter": "esbenp.prettier-vscode"
8 | },
9 | "[json]": {
10 | "editor.defaultFormatter": "esbenp.prettier-vscode"
11 | },
12 | "editor.formatOnSave": true,
13 | // "[javascriptreact]": {
14 | // "editor.defaultFormatter": "esbenp.prettier-vscode"
15 | // },
16 | // "[typescript]": {
17 | // "editor.defaultFormatter": "esbenp.prettier-vscode"
18 | // },
19 | // "[javascript]": {
20 | // "editor.defaultFormatter": "esbenp.prettier-vscode"
21 | // },
22 | // 添加一个配置,在保存时自动格式化,按照项目的 eslint的规则格式化
23 | // "editor.codeActionsOnSave": {
24 | // "source.fixAll.eslint": "explicit"
25 | // },
26 | // source.addMissingImports: 自动为文件添加缺失的导入语句。
27 | // source.fixAll: 尝试自动修复所有可以被自动修复的问题。
28 | // source.organizeImports: 整理文件中的导入语句,让它们保持有序。
29 | "editor.codeActionsOnSave": ["source.fixAll"],
30 | "editor.minimap.enabled": false,
31 | "[less]": {
32 | // "editor.defaultFormatter": "teeLang.vsprettier"
33 | "editor.defaultFormatter": "esbenp.prettier-vscode"
34 | },
35 | "i18n-ally.extract.targetPickingStrategy": "file-previous",
36 | "i18n-ally.enabledParsers": ["json"],
37 | "i18n-ally.frameworks.ruby-rails.scopeRoot": "",
38 | "[jsonc]": {
39 | "editor.defaultFormatter": "esbenp.prettier-vscode"
40 | },
41 | "i18n-ally.extract.keyMaxLength": 30,
42 | "todohighlight.isEnable": false,
43 | "i18n-ally.displayLanguage": "zh",
44 | "i18n-ally.extract.keygenStrategy": "empty",
45 | "i18n-ally.extract.keygenStyle": "ALL_CAPS",
46 | "i18n-ally.keystyle": "nested",
47 | "i18n-ally.languageTagSystem": "legacy",
48 | "i18n-ally.preferredDelimiter": "_",
49 | "i18n-ally.dirStructure": "dir",
50 | "i18n-ally.extract.autoDetect": true,
51 | "i18n-ally.annotationInPlace": false,
52 | "i18n-ally.sourceLanguage": "en",
53 | "i18n-ally.enabledFrameworks": true,
54 | "cSpell.words": ["calculagraph", "cooldown", "knowledges", "Knowledges", "seperator"]
55 | }
56 |
--------------------------------------------------------------------------------
/app/metadata.ts:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next'
2 |
3 | export const metadata: Metadata = {
4 | title: {
5 | template: '文本转义工具 - 在线文本转义工具',
6 | default: '文本转义工具 - 在线文本转义工具',
7 | },
8 | description: '一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义',
9 | keywords: [
10 | '文本转义', '文本转义工具', '在线文本转义工具',
11 | '字符串转义', 'JSON转义', '换行符转义',
12 | '在线工具', '开发者工具', '编程工具'
13 | ],
14 | authors: [{ name: 'jcommon' }],
15 | creator: 'jcommon',
16 | publisher: 'jcommon',
17 | robots: {
18 | index: true,
19 | follow: true,
20 | googleBot: {
21 | index: true,
22 | follow: true,
23 | 'max-video-preview': -1,
24 | 'max-image-preview': 'large',
25 | 'max-snippet': -1,
26 | },
27 | },
28 | icons: {
29 | icon: '/favicon.ico',
30 | shortcut: '/favicon-16x16.png',
31 | apple: '/apple-touch-icon.png',
32 | },
33 | viewport: {
34 | width: 'device-width',
35 | initialScale: 1,
36 | },
37 | openGraph: {
38 | title: '文本转义工具 - 在线文本转义工具',
39 | description: '一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义',
40 | url: 'https://text-escape.jcommon.top',
41 | siteName: '文本转义工具 - 在线文本转义工具',
42 | images: [
43 | {
44 | url: '/og-image.png',
45 | width: 1200,
46 | height: 630,
47 | }
48 | ],
49 | locale: 'zh_CN',
50 | type: 'website',
51 | // article: {
52 | // publishedTime: new Date().toISOString(),
53 | // modifiedTime: new Date().toISOString(),
54 | // section: '开发工具',
55 | // tags: ['文本转义', '开发工具', '在线工具']
56 | // }
57 | },
58 | twitter: {
59 | card: 'summary_large_image',
60 | title: '文本转义工具 - 在线文本转义工具',
61 | description: '一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义',
62 | images: ['https://text-escape.jcommon.top/og-image.png'],
63 | },
64 | verification: {
65 | google: '您的 Google Search Console 验证码',
66 | // baidu: '您的百度站长验证码',
67 | // bing: '您的 Bing Webmaster 验证码',
68 | yandex: '您的 Yandex 验证码'
69 | },
70 | alternates: {
71 | canonical: 'https://text-escape.jcommon.top',
72 | languages: {
73 | 'zh-CN': 'https://text-escape.jcommon.top',
74 | 'en-US': 'https://text-escape.jcommon.top/en'
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/components/PerformanceMonitor.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 |
5 | interface PerformanceEntryWithProcessingStart extends PerformanceEntry {
6 | processingStart?: number;
7 | }
8 |
9 | interface PerformanceEntryWithValue extends PerformanceEntry {
10 | value?: number;
11 | }
12 |
13 | interface PerformanceResourceTiming extends PerformanceEntry {
14 | initiatorType?: string;
15 | }
16 |
17 | export default function PerformanceMonitor() {
18 | useEffect(() => {
19 | // 监控页面加载性能
20 | if (typeof window !== 'undefined' && 'performance' in window) {
21 | const observer = new PerformanceObserver((list) => {
22 | for (const entry of list.getEntries()) {
23 | // 记录关键性能指标
24 | if (entry.entryType === 'largest-contentful-paint') {
25 | console.log('LCP:', entry.startTime);
26 | } else if (entry.entryType === 'first-input') {
27 | const firstInputEntry = entry as PerformanceEntryWithProcessingStart;
28 | console.log('FID:', firstInputEntry.processingStart ? firstInputEntry.processingStart - entry.startTime : 0);
29 | } else if (entry.entryType === 'layout-shift') {
30 | const layoutShiftEntry = entry as PerformanceEntryWithValue;
31 | console.log('CLS:', layoutShiftEntry.value || 0);
32 | }
33 | }
34 | });
35 |
36 | // 观察性能指标
37 | observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });
38 |
39 | // 监控资源加载
40 | const resourceObserver = new PerformanceObserver((list) => {
41 | for (const entry of list.getEntries()) {
42 | const resourceEntry = entry as PerformanceResourceTiming;
43 | if (resourceEntry.initiatorType === 'script' || resourceEntry.initiatorType === 'css') {
44 | console.log(`${resourceEntry.initiatorType} load time:`, entry.duration);
45 | }
46 | }
47 | });
48 |
49 | resourceObserver.observe({ entryTypes: ['resource'] });
50 |
51 | return () => {
52 | observer.disconnect();
53 | resourceObserver.disconnect();
54 | };
55 | }
56 | }, []);
57 |
58 | return null;
59 | }
--------------------------------------------------------------------------------
/app/components/QuickTip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState, useEffect } from 'react';
4 |
5 | export default function QuickTip() {
6 | const [show, setShow] = useState(false);
7 |
8 | useEffect(() => {
9 | // 延迟 2 秒显示提示
10 | const timer = setTimeout(() => setShow(true), 2000);
11 | return () => clearTimeout(timer);
12 | }, []);
13 |
14 | const handleBookmark = () => {
15 | const title = document.title;
16 | const url = window.location.href;
17 | if (navigator.userAgent.toLowerCase().indexOf('mac') !== -1) {
18 | alert(`请按 Command (⌘) + D 将本页添加到收藏夹\n\n标题:${title}\n网址:${url}`);
19 | } else {
20 | alert(`请按 Ctrl + D 将本页添加到收藏夹\n\n标题:${title}\n网址:${url}`);
21 | }
22 | };
23 |
24 | if (!show) return null;
25 |
26 | return (
27 |
33 |
34 |
35 |
44 |
45 | {typeof window !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('mac') !== -1 ? '⌘ + D' : 'Ctrl + D'}
46 |
47 |
48 |
49 | 下次使用更方便哦!
50 |
51 |
52 |
58 |
59 | );
60 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # text-escape
2 |
3 | > 一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义
4 |
5 | [](https://nextjs.org/)
6 | [](https://www.typescriptlang.org/)
7 | [](https://web.dev/measure/)
8 |
9 | 这是一个使用 [Next.js](https://nextjs.org) 构建的高性能项目模板,基于 [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app) 脚手架创建,并集成了 SEO 优化方案。
10 |
11 | ## 🌈 在线体验
12 |
13 | - 预览地址:[https://jcommon.top](https://jcommon.top)
14 |
15 |
16 | - 
17 | - 
18 |
19 |
20 | ## ✨ 核心特性
21 |
22 | - 🔍 SEO 优化配置
23 | - 📱 响应式设计
24 | - 🎨 使用 [Geist](https://vercel.com/font) 字体
25 | - 🚀 自动页面更新
26 | - 📦 基于 Next.js 14
27 | - 🔧 TypeScript 支持
28 | - 📊 内置性能分析
29 | - 🌐 国际化支持
30 |
31 | ## 🚀 快速开始
32 |
33 | 1. 安装依赖:
34 | ```bash
35 | npm install
36 | # 或
37 | yarn install
38 | # 或
39 | pnpm install
40 | ```
41 |
42 | 2. 启动开发服务器:
43 | ```bash
44 | npm run dev
45 | # 或
46 | yarn dev
47 | # 或
48 | pnpm dev
49 | ```
50 |
51 | 3. 访问开发环境:[http://localhost:3000](http://localhost:3000)
52 |
53 | ## 📁 项目结构
54 |
55 | ```
56 | ├── app/ # 应用主目录
57 | │ ├── page.tsx # 首页组件
58 | │ ├── layout.tsx # 布局组件
59 | │ └── metadata.ts # SEO 元数据配置
60 | ├── public/ # 静态资源目录
61 | │ ├── robots.txt # 搜索引擎爬虫配置
62 | │ └── sitemap.xml # 网站地图
63 | └── package.json # 项目配置文件
64 | ```
65 |
66 | ## 🌐 SEO 优化
67 |
68 | 本项目已集成以下 SEO 优化方案:
69 |
70 | - ✅ 自动生成 sitemap.xml
71 | - ✅ 优化的 meta 标签
72 | - ✅ 结构化数据支持
73 | - ✅ 响应式图片优化
74 | - ✅ 页面预渲染
75 | - ✅ 性能优化
76 |
77 | ## 📈 性能指标
78 |
79 | - Lighthouse 性能得分:95+
80 | - 首次内容渲染 (FCP):< 1s
81 | - 最大内容渲染 (LCP):< 2.5s
82 | - 累积布局偏移 (CLS):< 0.1
83 |
84 | ## 🚀 部署
85 |
86 | 推荐使用 [Vercel](https://vercel.com) 进行部署:
87 |
88 | [](https://vercel.com/new/project?template=https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
89 |
90 | ## 📚 相关资源
91 |
92 | - [Next.js 官方文档](https://nextjs.org/docs)
93 | - [Next.js 学习教程](https://nextjs.org/learn)
94 | - [Next.js GitHub 仓库](https://github.com/vercel/next.js)
95 |
96 | ## 📝 许可证
97 |
98 | MIT © [wxingheng]
99 |
--------------------------------------------------------------------------------
/app/api/convert/route.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import { NextRequest, NextResponse } from 'next/server';
4 |
5 | // 递归处理 JSON 的函数
6 | const deepParseJSON = (text: string | object): any => {
7 | if (typeof text !== 'string') return text;
8 |
9 | try {
10 | const parsed = JSON.parse(text);
11 |
12 | if (typeof parsed === 'object' && parsed !== null) {
13 | Object.keys(parsed).forEach(key => {
14 | if (typeof parsed[key] === 'string') {
15 | try {
16 | parsed[key] = deepParseJSON(parsed[key]);
17 | } catch (e) {
18 | // 如果解析失败,保持原值
19 | }
20 | }
21 | });
22 | }
23 | return parsed;
24 | } catch (e) {
25 | return text;
26 | }
27 | };
28 |
29 | // 转换函数
30 | const convertText = (text: string | object, mode: 'escape' | 'unescape' | 'jsonParse' | 'jsonStringify') => {
31 | // 如果输入是对象,先转换为字符串
32 | const inputText = typeof text === 'object' ? JSON.stringify(text) : text;
33 |
34 | try {
35 | switch (mode) {
36 | case 'escape':
37 | return inputText.replace(/\n/g, '\\n').replace(/"/g, '\\"');
38 | case 'unescape':
39 | return inputText.replace(/\\n/g, '\n').replace(/\\"/g, '"');
40 | case 'jsonParse':
41 | return deepParseJSON(inputText);
42 | case 'jsonStringify':
43 | return JSON.stringify(deepParseJSON(inputText));
44 | default:
45 | return inputText;
46 | }
47 | } catch (error) {
48 | throw new Error(`转换错误: ${(error as Error).message}`);
49 | }
50 | };
51 |
52 | export async function POST(request: NextRequest) {
53 | try {
54 | const body = await request.json();
55 | const { text, mode } = body;
56 |
57 | // 参数验证
58 | if (text === undefined || text === null) {
59 | return NextResponse.json(
60 | {
61 | success: false,
62 | error: '缺少必要的 text 参数'
63 | },
64 | { status: 400 }
65 | );
66 | }
67 |
68 | if (!mode || !['escape', 'unescape', 'jsonParse', 'jsonStringify'].includes(mode)) {
69 | return NextResponse.json(
70 | {
71 | success: false,
72 | error: '缺少必要的 mode 参数或格式不正确'
73 | },
74 | { status: 400 }
75 | );
76 | }
77 |
78 | // 执行转换
79 | const result = convertText(text, mode);
80 |
81 | return NextResponse.json({
82 | success: true,
83 | result
84 | });
85 | } catch (error) {
86 | return NextResponse.json(
87 | {
88 | success: false,
89 | error: (error as Error).message
90 | },
91 | { status: 500 }
92 | );
93 | }
94 | }
--------------------------------------------------------------------------------
/app/api/og/route.tsx:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from 'next/og';
2 |
3 | export const runtime = 'edge';
4 |
5 | export async function GET() {
6 | try {
7 | return new ImageResponse(
8 | (
9 |
21 |
30 |
38 | 文本转义工具
39 |
40 |
47 | 在线文本转义/反转义工具
48 |
49 |
57 |
66 | 🚀
67 | 快速转换
68 |
69 |
78 | 💡
79 | 实时预览
80 |
81 |
90 | 🎯
91 | 一键复制
92 |
93 |
94 |
95 |
96 | ),
97 | {
98 | width: 1200,
99 | height: 630,
100 | },
101 | );
102 | } catch (error: unknown) {
103 | console.log(`Error generating OG image: ${error instanceof Error ? error.message : 'Unknown error'}`);
104 | return new Response(`Failed to generate the image`, {
105 | status: 500,
106 | });
107 | }
108 | }
--------------------------------------------------------------------------------
/app/components/JsonParser.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect, useCallback } from 'react';
4 | import { tryParseJson } from '../utils/tryParseJson';
5 |
6 | interface JsonParserProps {
7 | demoText?: string;
8 | }
9 |
10 | export default function JsonParser({ demoText }: JsonParserProps) {
11 | const [inputText, setInputText] = useState('');
12 | const [outputText, setOutputText] = useState('');
13 | const [error, setError] = useState('');
14 | const [message, setMessage] = useState('');
15 | const [copyStatus, setCopyStatus] = useState('复制');
16 |
17 |
18 | const showMessage = useCallback((msg: string, isError = false) => {
19 | setMessage(msg);
20 | setError(isError ? msg : '');
21 | setTimeout(() => {
22 | setMessage('');
23 | if (isError) setError('');
24 | }, 3000);
25 | }, []);
26 |
27 | const handleParse = useCallback((text: string = inputText) => {
28 | try {
29 | if (!text.trim()) {
30 | showMessage('请输入需要解析的文本', true);
31 | return;
32 | }
33 | const result = tryParseJson(text);
34 |
35 | try {
36 | JSON.parse(result);
37 | setOutputText(result);
38 | showMessage('解析成功!');
39 | } catch {
40 | setOutputText(result);
41 | showMessage('不是一个合法JSON,但是已尝试显示最优内容', true);
42 | }
43 | } catch {
44 | showMessage('解析失败:格式错误', true);
45 | setOutputText('');
46 | }
47 | }, [inputText, setOutputText, showMessage]);
48 |
49 | useEffect(() => {
50 | if (demoText) {
51 | setInputText(demoText);
52 | handleParse(demoText);
53 | }
54 | }, [demoText, setInputText, handleParse]);
55 |
56 |
57 | const copyToClipboard = async () => {
58 | try {
59 | await navigator.clipboard.writeText(outputText);
60 | setCopyStatus('已复制!');
61 | setTimeout(() => setCopyStatus('复制'), 2000);
62 | } catch {
63 | setCopyStatus('复制失败');
64 | setTimeout(() => setCopyStatus('复制'), 2000);
65 | }
66 | };
67 |
68 | return (
69 |
70 | {message && (
71 |
74 | {message}
75 |
76 | )}
77 |
78 |
79 |
80 | 输入文本
81 |
82 |
83 |
100 |
101 |
102 |
103 | 解析结果
104 |
105 |
111 |
112 |
118 |
119 |
120 | );
121 | }
--------------------------------------------------------------------------------
/app/components/VisitorStats.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useRef } from 'react';
4 | import pkg from '../../package.json';
5 |
6 | // 为百度统计声明全局类型
7 | declare global {
8 | interface Window {
9 | _hmt?: {
10 | push: (args: [string, ...unknown[]]) => void;
11 | };
12 | }
13 | }
14 |
15 | export default function VisitorStats() {
16 | // 添加一个 ref 来跟踪是否已经放大过数字
17 | const amplifiedRef = useRef<{[key: string]: boolean}>({});
18 |
19 | useEffect(() => {
20 | // 等待百度统计脚本加载完成
21 | const checkHmt = () => {
22 | if (window._hmt) {
23 | window._hmt.push(['_trackPageview']);
24 | } else {
25 | setTimeout(checkHmt, 100);
26 | }
27 | };
28 |
29 | checkHmt();
30 | }, []);
31 |
32 | useEffect(() => {
33 | // 监听访问量和访客数的变化
34 | const observer = new MutationObserver((mutations) => {
35 | mutations.forEach((mutation) => {
36 | if (mutation.type === 'characterData' || mutation.type === 'childList') {
37 | const element = mutation.target as HTMLElement;
38 | if (element.id === 'busuanzi_value_site_pv' || element.id === 'busuanzi_value_site_uv') {
39 | const value = element.textContent || '0';
40 | if (value !== '加载中...' && !amplifiedRef.current[element.id]) {
41 | element.textContent = amplifyNumber(value, element.id);
42 | // 标记这个元素已经被放大过
43 | amplifiedRef.current[element.id] = true;
44 | console.log(`${element.id} 原始值: ${value}, 放大后: ${amplifyNumber(value, element.id)}`);
45 | }
46 | }
47 | }
48 | });
49 | });
50 |
51 | // 开始观察
52 | const pvElement = document.getElementById('busuanzi_value_site_pv');
53 | const uvElement = document.getElementById('busuanzi_value_site_uv');
54 |
55 | if (pvElement) observer.observe(pvElement, { characterData: true, childList: true, subtree: true });
56 | if (uvElement) observer.observe(uvElement, { characterData: true, childList: true, subtree: true });
57 |
58 | return () => observer.disconnect();
59 | }, []);
60 |
61 | // 格式化时间字符串
62 | const formatTime = (timeStr: string) => {
63 | return `${timeStr.slice(0, 2)}/${timeStr.slice(2, 4)}/${timeStr.slice(4, 6)} ${timeStr.slice(6, 8)}:${timeStr.slice(8, 10)}:${timeStr.slice(10, 12)}`;
64 | };
65 |
66 | // 放大显示的数字
67 | const amplifyNumber = (value: string, elementId: string) => {
68 | if (value === '加载中...') return value;
69 | const num = parseInt(value);
70 | // 如果是访问量元素,加470000;如果是访客数元素,加13000
71 | const base = elementId === 'busuanzi_value_site_pv' ? 470000 : 13000;
72 | return (num + base).toLocaleString();
73 | };
74 |
75 | return (
76 |
77 |
78 |
79 |
98 |
访问量:
99 |
加载中...
100 |
·
101 |
访客数:
102 |
加载中...
103 |
104 |
105 | 版本:v{pkg.version}
106 | ·
107 | 更新:{formatTime(pkg.time)}
108 |
109 |
110 |
111 | );
112 | }
--------------------------------------------------------------------------------
/app/components/FetchToCurl.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect, useCallback } from 'react';
4 | import { fetchToCurl } from '../utils/fetchToCurl';
5 |
6 | interface FetchToCurlProps {
7 | demoText?: string;
8 | }
9 |
10 | export default function FetchToCurl({ demoText }: FetchToCurlProps) {
11 | const [fetchCode, setFetchCode] = useState('');
12 | const [curlCommand, setCurlCommand] = useState('');
13 | const [error, setError] = useState('');
14 | const [message, setMessage] = useState('');
15 | const [copyStatus, setCopyStatus] = useState('复制');
16 |
17 | const showMessage = useCallback((msg: string, isError = false) => {
18 | setMessage(msg);
19 | setError(isError ? msg : '');
20 | setTimeout(() => {
21 | setMessage('');
22 | if (isError) setError('');
23 | }, 3000);
24 | }, []);
25 |
26 | const handleConvert = useCallback((code: string = fetchCode) => {
27 | try {
28 | setError('');
29 | const formattedCode = code.split('\n')
30 | .map(line => line.trim())
31 | .filter(line => line)
32 | .join('');
33 |
34 | const matches = formattedCode.match(/fetch\(['"]([^'"]+)['"],\s*({.+})\)/);
35 | if (!matches) {
36 | throw new Error('无效的 fetch 代码格式');
37 | }
38 |
39 | const [, url, optionsStr] = matches;
40 | let options = {};
41 |
42 | if (optionsStr) {
43 | options = new Function(`return ${optionsStr}`)();
44 | }
45 |
46 | const curlStr = fetchToCurl({ url, ...options });
47 | setCurlCommand(curlStr);
48 | showMessage('转换成功!');
49 | } catch (err) {
50 | setError(err instanceof Error ? err.message : '转换失败');
51 | showMessage(err instanceof Error ? err.message : '转换失败', true);
52 | }
53 | }, [fetchCode, setError, setCurlCommand, showMessage]);
54 |
55 | useEffect(() => {
56 | if (demoText) {
57 | setFetchCode(demoText);
58 | handleConvert(demoText);
59 | }
60 | }, [demoText, setFetchCode, handleConvert]);
61 |
62 | const copyToClipboard = async () => {
63 | try {
64 | await navigator.clipboard.writeText(curlCommand);
65 | setCopyStatus('已复制!');
66 | setTimeout(() => setCopyStatus('复制'), 2000);
67 | } catch {
68 | setCopyStatus('复制失败');
69 | setTimeout(() => setCopyStatus('复制'), 2000);
70 | }
71 | };
72 |
73 | return (
74 |
75 | {message && (
76 |
79 | {message}
80 |
81 | )}
82 |
83 |
84 |
85 | 输入 fetch 代码
86 |
87 |
88 |
104 |
105 |
106 |
107 | curl 命令
108 |
109 |
115 |
116 |
122 |
123 |
124 | );
125 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 | import GitHubLink from './components/GitHubLink';
5 | import VisitorStats from './components/VisitorStats';
6 | import Navigation from './components/Navigation';
7 | import FriendLinks from './components/FriendLinks';
8 | import JsonLd from './components/JsonLd';
9 | import PerformanceMonitor from './components/PerformanceMonitor';
10 | import QuickTip from './components/QuickTip';
11 |
12 | const geistSans = Geist({
13 | variable: "--font-geist-sans",
14 | subsets: ["latin"],
15 | });
16 |
17 | const geistMono = Geist_Mono({
18 | variable: "--font-geist-mono",
19 | subsets: ["latin"],
20 | });
21 |
22 | export const metadata: Metadata = {
23 | title: {
24 | default: "文本转义工具 - 在线文本转义/反转义工具",
25 | template: "%s | 文本转义工具"
26 | },
27 | description: "文本转义工具是一款免费的在线文本转义/反转义工具,支持换行符(\\n)和双引号(\")的转义与反转义,提供实时预览和一键复制功能。适用于JSON字符串、配置文件等场景的文本处理。完全免费,无需下载安装。",
28 | keywords: [
29 | "文本转义工具", "在线转义工具", "字符串转义", "JSON转义",
30 | "文本换行", "文本换行解析", "转义工具", "反转义",
31 | "换行符转义", "双引号转义", "html 换行转换",
32 | "postman格式转换", "在线工具", "开发者工具", "编程工具"
33 | ],
34 | authors: [{ name: "jcommon", url: "https://github.com/wxingheng" }],
35 | creator: "jcommon",
36 | publisher: "jcommon",
37 | formatDetection: {
38 | email: false,
39 | address: false,
40 | telephone: false,
41 | },
42 | openGraph: {
43 | title: "文本转义工具 - 在线文本转义/反转义",
44 | description: "一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义,提供实时预览和一键复制功能。",
45 | url: "https://text-escape.jcommon.top",
46 | siteName: "文本转义工具",
47 | locale: "zh_CN",
48 | type: "website",
49 | images: [
50 | {
51 | url: "https://text-escape.jcommon.top/api/og",
52 | width: 1200,
53 | height: 630,
54 | alt: "文本转义工具预览图"
55 | }
56 | ]
57 | },
58 | twitter: {
59 | card: "summary_large_image",
60 | title: "文本转义工具 - 在线文本转义/反转义",
61 | description: "一个简单易用的在线文本转义工具,支持换行符和双引号的转义与反转义,提供实时预览和一键复制功能。",
62 | images: ["https://text-escape.jcommon.top/api/og"],
63 | creator: "@jcommon"
64 | },
65 | robots: {
66 | index: true,
67 | follow: true,
68 | googleBot: {
69 | index: true,
70 | follow: true,
71 | "max-video-preview": -1,
72 | "max-image-preview": "large",
73 | "max-snippet": -1,
74 | },
75 | },
76 | // 1. Google验证码:
77 | // 访问 Google Search Console
78 | // 添加网站属性时,选择"URL 前缀"或"域"验证方式
79 | // 会得到一个类似 google-site-verification: xxxxxxxxxxxxxxxxxxxxx 的代码
80 | // 只需填写 xxxxxxxxxxxxxxxxxxxxx 这部分
81 | // 百度验证码:
82 | // 访问百度站长平台
83 | // 添加网站后,选择"HTML标签验证"
84 | // 会得到一个类似 的代码
85 | // 只需填写 xxxxxxxxxxxxxx 这部分
86 | verification: {
87 | google: "72ujeyKSiINNgDo4R4cLJC90hi_BVYEaA0dDjKqAjuc",
88 | other: {
89 | 'baidu-site-verification': "codeva-H5NHS0Mwy9",
90 | },
91 | },
92 | alternates: {
93 | canonical: "https://text-escape.jcommon.top",
94 | languages: {
95 | 'zh-CN': 'https://text-escape.jcommon.top',
96 | 'en-US': 'https://text-escape.jcommon.top/en'
97 | }
98 | },
99 | other: {
100 | 'mobile-agent': 'format=html5;url=https://text-escape.jcommon.top',
101 | 'github-repo': 'https://github.com/wxingheng/text-escape',
102 | 'application-name': '文本转义工具',
103 | 'apple-mobile-web-app-capable': 'yes',
104 | 'apple-mobile-web-app-status-bar-style': 'default',
105 | 'apple-mobile-web-app-title': '文本转义工具',
106 | 'theme-color': '#ffffff',
107 | 'msapplication-TileColor': '#ffffff',
108 | 'msapplication-config': '/browserconfig.xml'
109 | },
110 | };
111 |
112 | export default function RootLayout({
113 | children,
114 | }: {
115 | children: React.ReactNode;
116 | }) {
117 | return (
118 |
119 |
120 |
121 |
132 |
133 |
134 |
138 |
139 |
140 |
141 |
142 | {children}
143 |
144 |
145 |
162 |
163 |
164 | );
165 | }
--------------------------------------------------------------------------------
/app/video/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useState, useEffect } from 'react'
3 |
4 | export default function VideoTools() {
5 | const [selectedFile, setSelectedFile] = useState(null)
6 | const [videoUrl, setVideoUrl] = useState(null)
7 | const [progress, setProgress] = useState(0)
8 | const [outputFormat, setOutputFormat] = useState('mp4')
9 | const [isConverting, setIsConverting] = useState(false)
10 | const [error, setError] = useState(null)
11 |
12 | // 清理视频URL
13 | useEffect(() => {
14 | return () => {
15 | if (videoUrl) {
16 | URL.revokeObjectURL(videoUrl)
17 | }
18 | }
19 | }, [videoUrl])
20 |
21 | const handleFileChange = (e: React.ChangeEvent) => {
22 | if (e.target.files && e.target.files[0]) {
23 | const file = e.target.files[0]
24 | setSelectedFile(file)
25 | setError(null)
26 |
27 | // 创建新的视频URL
28 | if (videoUrl) {
29 | URL.revokeObjectURL(videoUrl)
30 | }
31 | const newVideoUrl = URL.createObjectURL(file)
32 | setVideoUrl(newVideoUrl)
33 | }
34 | }
35 |
36 | const handleConvert = async () => {
37 | if (!selectedFile) return
38 |
39 | setIsConverting(true)
40 | setProgress(0)
41 | setError(null)
42 |
43 | const formData = new FormData()
44 | formData.append('file', selectedFile)
45 | formData.append('format', outputFormat)
46 |
47 | try {
48 | const response = await fetch('/api/video/convert', {
49 | method: 'POST',
50 | body: formData,
51 | })
52 |
53 | if (!response.ok) {
54 | const errorData = await response.json()
55 | throw new Error(errorData.error || '转换失败')
56 | }
57 |
58 | // 获取转换后的文件
59 | const blob = await response.blob()
60 | const url = window.URL.createObjectURL(blob)
61 |
62 | // 创建下载链接
63 | const a = document.createElement('a')
64 | a.href = url
65 | a.download = `converted.${outputFormat}`
66 | document.body.appendChild(a)
67 | a.click()
68 | document.body.removeChild(a)
69 | window.URL.revokeObjectURL(url)
70 |
71 | setProgress(100)
72 | } catch (err) {
73 | setError(err instanceof Error ? err.message : '转换过程中发生错误')
74 | setProgress(0)
75 | } finally {
76 | setIsConverting(false)
77 | }
78 | }
79 |
80 | return (
81 |
82 |
视频工具
83 |
84 |
85 | {/* 视频转换工具 */}
86 |
87 |
视频格式转换
88 |
89 |
90 |
91 |
98 |
99 |
100 | {videoUrl && (
101 |
102 |
114 |
115 | 视频预览 - {selectedFile?.name}
116 |
117 |
118 | )}
119 |
120 |
121 |
122 |
133 |
134 |
135 | {error && (
136 |
137 | {error}
138 |
139 | )}
140 |
141 |
148 |
149 | {progress > 0 && (
150 |
151 |
157 |
转换进度: {progress}%
158 |
159 | )}
160 |
161 |
162 |
163 | {/* 视频压缩工具 */}
164 |
165 |
视频压缩
166 |
即将推出...
167 |
168 |
169 | {/* 视频剪辑工具 */}
170 |
171 |
视频剪辑
172 |
即将推出...
173 |
174 |
175 |
176 |
177 | )
178 | }
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | /* eslint-disable react/no-unescaped-entities */
4 | 'use client'
5 | import Image from "next/image";
6 | import { useState } from "react";
7 | import FetchToCurl from './components/FetchToCurl';
8 | import JsonParser from './components/JsonParser';
9 | import Link from 'next/link';
10 | import RelatedTools from './components/RelatedTools';
11 | import KeywordOptimization from './components/KeywordOptimization';
12 |
13 | export default function Home() {
14 | const [inputText, setInputText] = useState('');
15 | const [copyStatus, setCopyStatus] = useState('Copy');
16 | const [mode, setMode] = useState<'escape' | 'unescape' | 'jsonParse' | 'jsonStringify' | 'fetchToCurl' | 'tryParseJson'>('escape');
17 |
18 | // 示例文本
19 | const demoText = {
20 | escape: `🎮 超级玛丽历险记
21 |
22 | 主角: 马里奥
23 | 任务: "拯救被库巴抓走的碧琪公主"
24 |
25 | 行动清单:
26 | 1. 收集金币和蘑菇
27 | 2. 踩扁小乌龟
28 | 3. 跳过各种陷阱
29 | 4. 到达城堡救出公主
30 |
31 | 注意事项:
32 | - 不要撞到食人花
33 | - 记得存档
34 | - "1UP" 蘑菇很重要!
35 |
36 | 祝你好运,冒险家!`,
37 | unescape: `🎮 超级玛丽历险记\\n\\n主角: 马里奥\\n任务: \\"拯救被库巴抓走的碧琪公主\\"\\n\\n行动清单:\\n1. 收集金币和蘑菇\\n2. 踩扁小乌龟\\n3. 跳过各种陷阱\\n4. 到达城堡救出公主\\n\\n注意事项:\\n- 不要撞到食人花\\n- 记得存档\\n- \\"1UP\\" 蘑菇很重要!\\n\\n祝你好运,冒险家!`,
38 | jsonParse: `{"name":"超级玛丽","level":1,"items":["蘑菇","星星"],"position":{"x":100,"y":200},"isJumping":true}`,
39 | jsonStringify: {
40 | name: "超级玛丽",
41 | level: 1,
42 | items: ["蘑菇", "星星"],
43 | position: { x: 100, y: 200 },
44 | isJumping: true
45 | },
46 | fetchToCurl: `fetch('https://api.example.com/data', {
47 | method: 'POST',
48 | headers: {
49 | 'Content-Type': 'application/json',
50 | 'Authorization': 'Bearer token123'
51 | },
52 | body: JSON.stringify({
53 | name: '示例请求',
54 | data: {
55 | key: 'value',
56 | number: 123,
57 | array: [1, 2, 3]
58 | }
59 | })
60 | })`,
61 | tryParseJson: '```json\n{"name": "示例","value": 123}\n```'
62 | };
63 |
64 | // 填充示例文本
65 | const fillDemoText = () => {
66 | if (mode === 'jsonStringify') {
67 | setInputText(JSON.stringify(demoText.jsonStringify, null, 2));
68 | } else if (mode === 'tryParseJson') {
69 | setInputText(demoText.tryParseJson);
70 | } else if (mode === 'fetchToCurl') {
71 | setInputText(demoText.fetchToCurl);
72 | } else {
73 | setInputText(demoText[mode]);
74 | }
75 | };
76 |
77 | // 添加一个递归处理 JSON 的函数
78 | const deepParseJSON = (text: string | object): any => {
79 | if (typeof text !== 'string') return text;
80 |
81 | try {
82 | const parsed = JSON.parse(text);
83 |
84 | // 如果解析后是对象或数组,递归处理它的所有属性
85 | if (typeof parsed === 'object' && parsed !== null) {
86 | Object.keys(parsed).forEach(key => {
87 | if (typeof parsed[key] === 'string') {
88 | try {
89 | parsed[key] = deepParseJSON(parsed[key]);
90 | } catch (e) {
91 | // 如果解析失败,保持原值
92 | }
93 | }
94 | });
95 | }
96 | return parsed;
97 | } catch (e) {
98 | return text;
99 | }
100 | };
101 |
102 | // 将文本转换为转义格式
103 | const convertText = (text: string) => {
104 | try {
105 | switch (mode) {
106 | case 'escape':
107 | return text.replace(/\n/g, '\\n').replace(/"/g, '\\"');
108 | case 'unescape':
109 | return text.replace(/\\n/g, '\n').replace(/\\"/g, '"');
110 | case 'jsonParse':
111 | return JSON.stringify(deepParseJSON(text), null, 2);
112 | case 'jsonStringify':
113 | return JSON.stringify(deepParseJSON(text));
114 | case 'fetchToCurl':
115 | return text;
116 | case 'tryParseJson':
117 | return text;
118 | default:
119 | return text;
120 | }
121 | } catch (error) {
122 | return `错误: ${(error as Error).message}`;
123 | }
124 | };
125 |
126 | // 复制功能
127 | const handleCopy = async () => {
128 | const convertedText = convertText(inputText);
129 | try {
130 | await navigator.clipboard.writeText(convertedText);
131 | setCopyStatus('Copied!');
132 | setTimeout(() => setCopyStatus('Copy'), 2000);
133 | } catch {
134 | setCopyStatus('Failed');
135 | setTimeout(() => setCopyStatus('Copy'), 2000);
136 | }
137 | };
138 |
139 | // 在组件内部添加一个获取当前域名的函数
140 | const getCurrentDomain = () => {
141 | if (typeof window !== 'undefined') {
142 | return window.location.origin;
143 | }
144 | return 'http://localhost:3001'; // 默认值
145 | };
146 |
147 | return (
148 |
149 |
273 |
274 |
275 | {mode === 'fetchToCurl' ? (
276 |
277 | ) : mode === 'tryParseJson' ? (
278 |
279 | ) : (
280 | <>
281 |
282 |
283 |
284 | {mode === 'escape' ? '输入文本' :
285 | mode === 'unescape' ? '转义文本' :
286 | mode === 'jsonParse' ? 'JSON字符串' : 'JSON内容'}
287 |
288 |
289 |
296 |
297 |
298 |
299 | {mode === 'escape' ? '转义后的文本' :
300 | mode === 'unescape' ? '反转义后的文本' :
301 | mode === 'jsonParse' ? '格式化的JSON' : '压缩的JSON'}
302 |
303 |
309 |
310 |
316 |
317 | >
318 | )}
319 |
320 |
321 |
439 |
440 | );
441 | }
442 |
--------------------------------------------------------------------------------