Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c2799f7d5 | ||
|
|
dd70f9d886 | ||
|
|
35cccf6a8f | ||
| c5616600fa | |||
|
|
986708fbb4 | ||
|
|
6a1b68ed2c | ||
|
|
32970acf32 | ||
|
|
812bb8c248 | ||
|
|
09f9e6588f | ||
|
|
10a167febd | ||
|
|
91c0686a46 | ||
|
|
40bfde8e57 | ||
|
|
b4ba7a2219 | ||
|
|
25e42e3af5 | ||
|
|
4398b53ea7 | ||
|
|
3e14bc652f | ||
|
|
aca7e11835 | ||
|
|
782de6e38a | ||
|
|
7646830194 | ||
|
|
59f998e8e3 | ||
|
|
55207beff5 | ||
|
|
e98a344b95 | ||
|
|
d553c3e04c | ||
|
|
e2da2758cc | ||
|
|
3ab70498e6 | ||
|
|
a5ef1a1e70 | ||
|
|
297000f208 | ||
|
|
04fbf12e07 | ||
| a9a6354b2d | |||
|
|
109139a42e | ||
|
|
b5a811e5ee | ||
|
|
b3adfe5c8f | ||
|
|
8eda2eae99 | ||
|
|
99673913a6 | ||
|
|
83e48e3485 | ||
|
|
8607591871 |
@@ -43,8 +43,8 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max
|
||||
# cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache
|
||||
# cache-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
63
index.html
63
index.html
@@ -2,12 +2,71 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/lite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lite Kit</title>
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>Lite Kit - Lightweight Online Tools</title>
|
||||
<meta name="description" content="Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more. Fast, secure, and easy to use." />
|
||||
<meta name="keywords" content="online tools,UUID generator,JSON formatter,Base64 encoder,network tools,DNS lookup,Ping test,TCPing,speed test,IP query" />
|
||||
<meta name="author" content="Lite Kit" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/lite.svg" />
|
||||
|
||||
<!-- DNS Prefetch & Preconnect for external APIs -->
|
||||
<link rel="dns-prefetch" href="https://ipinfo.io">
|
||||
<link rel="preconnect" href="https://ipinfo.io" crossorigin>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://litek.typist.cc/" />
|
||||
<meta property="og:title" content="Lite Kit - Lightweight Online Tools" />
|
||||
<meta property="og:description" content="Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more." />
|
||||
<meta property="og:image" content="https://litek.typist.cc/lite.svg" />
|
||||
<meta property="og:site_name" content="Lite Kit" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Lite Kit - Lightweight Online Tools" />
|
||||
<meta name="twitter:description" content="Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more." />
|
||||
<meta name="twitter:image" content="https://litek.typist.cc/lite.svg" />
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<!-- Structured Data (JSON-LD) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "Lite Kit",
|
||||
"description": "Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more",
|
||||
"url": "https://litek.typist.cc/",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "Lite Kit"
|
||||
},
|
||||
"applicationCategory": "UtilitiesApplication",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Cloudflare Web Analytics -->
|
||||
<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "2aecdc025eb043bc89ce931b54a80054"}'></script>
|
||||
<!-- End Cloudflare Web Analytics -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
12
package.json
12
package.json
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "litek",
|
||||
"private": true,
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.27",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"generate:sitemap": "tsx scripts/generate-sitemap.ts",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"release:patch": "npm version patch && git push --follow-tags",
|
||||
@@ -43,17 +44,22 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"terser": "^5.44.0",
|
||||
"tsx": "^4.19.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "npm:rolldown-vite@7.1.14"
|
||||
"vite": "npm:rolldown-vite@7.1.14",
|
||||
"vite-plugin-pwa": "^1.1.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@7.1.14"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@swc/core"
|
||||
"@swc/core",
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
3108
pnpm-lock.yaml
generated
3108
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,19 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<style>
|
||||
/* 默认浅色模式使用黑色 */
|
||||
path {
|
||||
fill: #000000;
|
||||
}
|
||||
/* 暗色模式使用白色 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path {
|
||||
fill: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M12.6089 6.12601C12.6785 5.85878 12.5183 5.58573 12.251 5.51614C11.9838 5.44655 11.7108 5.60676 11.6412 5.87399L10.0842 11.8528L7.85646 12.5211C7.59196 12.6005 7.44187 12.8792 7.52122 13.1437C7.60057 13.4082 7.87931 13.5583 8.14381 13.479L9.78926 12.9853L8.51617 17.874C8.47715 18.0238 8.50974 18.1833 8.60443 18.3058C8.69911 18.4283 8.8452 18.5 9.00003 18.5H16C16.2762 18.5 16.5 18.2761 16.5 18C16.5 17.7239 16.2762 17.5 16 17.5H9.64691L10.9102 12.6491L13.1438 11.979C13.4083 11.8996 13.5584 11.6209 13.479 11.3564C13.3997 11.0919 13.121 10.9418 12.8565 11.0211L11.2051 11.5165L12.6089 6.12601Z"/> </g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
21
public/manifest.json
Normal file
21
public/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Lite Kit - Lightweight Online Tools",
|
||||
"short_name": "Lite Kit",
|
||||
"description": "Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#000000",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/lite.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "any"
|
||||
}
|
||||
],
|
||||
"categories": ["utilities", "productivity"],
|
||||
"lang": "en",
|
||||
"dir": "ltr"
|
||||
}
|
||||
|
||||
21
public/robots.txt
Normal file
21
public/robots.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
# Allow all crawlers
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: https://litek.typist.cc/sitemap.xml
|
||||
|
||||
# Common bots
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
|
||||
User-agent: Bingbot
|
||||
Allow: /
|
||||
|
||||
User-agent: Baiduspider
|
||||
Allow: /
|
||||
|
||||
# Crawl-delay for less aggressive bots
|
||||
User-agent: *
|
||||
Crawl-delay: 1
|
||||
|
||||
63
public/sitemap.xml
Normal file
63
public/sitemap.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://litek.typist.cc</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/uuid</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/json</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/base64</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/network/dns</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/network/ping</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/network/tcping</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/network/speedtest</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://litek.typist.cc/tool/network/ipquery</loc>
|
||||
<lastmod>2025-10-29</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
83
scripts/generate-sitemap.ts
Normal file
83
scripts/generate-sitemap.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
interface SitemapUrl {
|
||||
loc: string;
|
||||
lastmod: string;
|
||||
changefreq: string;
|
||||
priority: string;
|
||||
}
|
||||
|
||||
const BASE_URL = 'https://litek.typist.cc';
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
const tools = [
|
||||
{ path: '/tool/uuid', priority: '0.9' },
|
||||
{ path: '/tool/json', priority: '0.9' },
|
||||
{ path: '/tool/base64', priority: '0.9' },
|
||||
{ path: '/tool/network/dns', priority: '0.8' },
|
||||
{ path: '/tool/network/ping', priority: '0.8' },
|
||||
{ path: '/tool/network/tcping', priority: '0.8' },
|
||||
{ path: '/tool/network/speedtest', priority: '0.8' },
|
||||
{ path: '/tool/network/ipquery', priority: '0.8' },
|
||||
];
|
||||
|
||||
const urls: SitemapUrl[] = [
|
||||
{
|
||||
loc: BASE_URL,
|
||||
lastmod: currentDate,
|
||||
changefreq: 'weekly',
|
||||
priority: '1.0',
|
||||
},
|
||||
{
|
||||
loc: `${BASE_URL}/tool`,
|
||||
lastmod: currentDate,
|
||||
changefreq: 'weekly',
|
||||
priority: '0.9',
|
||||
},
|
||||
];
|
||||
|
||||
// Add all tool pages
|
||||
tools.forEach((tool) => {
|
||||
urls.push({
|
||||
loc: `${BASE_URL}${tool.path}`,
|
||||
lastmod: currentDate,
|
||||
changefreq: 'monthly',
|
||||
priority: tool.priority,
|
||||
});
|
||||
});
|
||||
|
||||
function generateSitemap(): string {
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
||||
|
||||
urls.forEach((url) => {
|
||||
xml += ' <url>\n';
|
||||
xml += ` <loc>${url.loc}</loc>\n`;
|
||||
xml += ` <lastmod>${url.lastmod}</lastmod>\n`;
|
||||
xml += ` <changefreq>${url.changefreq}</changefreq>\n`;
|
||||
xml += ` <priority>${url.priority}</priority>\n`;
|
||||
xml += ' </url>\n';
|
||||
});
|
||||
|
||||
xml += '</urlset>\n';
|
||||
return xml;
|
||||
}
|
||||
|
||||
// Generate and write sitemap
|
||||
const sitemap = generateSitemap();
|
||||
const publicDir = path.resolve(__dirname, '../public');
|
||||
const sitemapPath = path.join(publicDir, 'sitemap.xml');
|
||||
|
||||
// Ensure public directory exists
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(sitemapPath, sitemap, 'utf-8');
|
||||
console.log(`✅ Sitemap generated successfully at ${sitemapPath}`);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenuButton, SidebarMenuItem, SidebarMenu, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton } from "@/components/ui/sidebar";
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import { tools, type Tool } from "@/components/tool";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ModeToggle } from "@/components/theme/toggle";
|
||||
import { Button } from "../ui/button";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
|
||||
export const AppSidebar = () => {
|
||||
// 递归构建完整路径
|
||||
@@ -13,6 +13,46 @@ export const AppSidebar = () => {
|
||||
return `/tool/${pathSegments.join("/")}`;
|
||||
};
|
||||
|
||||
// 递归渲染子菜单内容
|
||||
const renderSubMenuContent = (child: Tool, currentPaths: string[]): ReactNode => {
|
||||
if (child.children) {
|
||||
// 子菜单内的可折叠项
|
||||
return (
|
||||
<Collapsible defaultOpen={false} className="group/collapsible">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuSubButton>
|
||||
{child.icon}
|
||||
<span>{child.name}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuSubButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{child.children.map((subChild) => (
|
||||
<SidebarMenuSubItem key={subChild.name}>
|
||||
{renderSubMenuContent(subChild, [...currentPaths, child.path])}
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
// 叶子节点
|
||||
return (
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to={buildFullPath([...currentPaths, child.path])}
|
||||
title={child.description}
|
||||
>
|
||||
{child.icon}
|
||||
<span>{child.name}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
);
|
||||
};
|
||||
|
||||
// 递归渲染菜单项
|
||||
const renderMenuItem = (tool: Tool, parentPaths: string[] = []): ReactNode => {
|
||||
const currentPaths = [...parentPaths, tool.path];
|
||||
@@ -20,12 +60,11 @@ export const AppSidebar = () => {
|
||||
if (tool.children) {
|
||||
// 有子菜单的项目
|
||||
return (
|
||||
<Collapsible
|
||||
key={tool.name}
|
||||
defaultOpen={false}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuItem key={tool.name}>
|
||||
<Collapsible
|
||||
defaultOpen={false}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={tool.description}>
|
||||
{tool.icon}
|
||||
@@ -37,25 +76,13 @@ export const AppSidebar = () => {
|
||||
<SidebarMenuSub>
|
||||
{tool.children.map((child) => (
|
||||
<SidebarMenuSubItem key={child.name}>
|
||||
{child.children ? (
|
||||
renderMenuItem(child, currentPaths)
|
||||
) : (
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
to={buildFullPath([...currentPaths, child.path])}
|
||||
title={child.description}
|
||||
>
|
||||
{child.icon}
|
||||
<span>{child.name}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
)}
|
||||
{renderSubMenuContent(child, currentPaths)}
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</Collapsible>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,10 +115,9 @@ export const AppSidebar = () => {
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="flex flex-row justify-between items-center gap-2">
|
||||
<Button variant="link">
|
||||
<SidebarMenuButton variant="outline" asChild>
|
||||
<a href="mailto:litek@mail.typist.cc">need more tools?</a>
|
||||
</Button>
|
||||
<ModeToggle />
|
||||
</SidebarMenuButton>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { lazy, type ReactNode, type ComponentType } from 'react';
|
||||
import { FileJson, Hash, Binary, Network, Globe, Activity, Gauge, Wifi, MapPin } from 'lucide-react'
|
||||
|
||||
import UUID from './uuid'
|
||||
import JSON from './json'
|
||||
import Base64 from './base64'
|
||||
import { DNS, Ping, TCPing, SpeedTest, IPQuery } from './network'
|
||||
// 懒加载工具组件
|
||||
const UUID = lazy(() => import('./uuid'))
|
||||
const JSON = lazy(() => import('./json'))
|
||||
const Base64 = lazy(() => import('./base64'))
|
||||
const DNS = lazy(() => import('./network/dns'))
|
||||
const Ping = lazy(() => import('./network/ping'))
|
||||
const TCPing = lazy(() => import('./network/tcping'))
|
||||
const SpeedTest = lazy(() => import('./network/speedtest'))
|
||||
const IPQuery = lazy(() => import('./network/ipquery'))
|
||||
|
||||
export interface Tool {
|
||||
path: string;
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
description: string;
|
||||
component?: ReactNode;
|
||||
component?: ComponentType;
|
||||
children?: Tool[];
|
||||
}
|
||||
|
||||
@@ -21,21 +26,21 @@ export const tools: Tool[] = [
|
||||
name: "UUID Generator",
|
||||
description: "Generate a UUID",
|
||||
icon: <Hash />,
|
||||
component: <UUID />,
|
||||
component: UUID,
|
||||
},
|
||||
{
|
||||
path: "json",
|
||||
name: "JSON Formatter",
|
||||
description: "Format and validate JSON",
|
||||
icon: <FileJson />,
|
||||
component: <JSON />,
|
||||
component: JSON,
|
||||
},
|
||||
{
|
||||
path: "base64",
|
||||
name: "Base64 Encoder/Decoder",
|
||||
description: "Encode and decode Base64",
|
||||
icon: <Binary />,
|
||||
component: <Base64 />,
|
||||
component: Base64,
|
||||
},
|
||||
{
|
||||
path: "network",
|
||||
@@ -48,35 +53,35 @@ export const tools: Tool[] = [
|
||||
name: "DNS Lookup",
|
||||
description: "DNS query tool",
|
||||
icon: <Globe />,
|
||||
component: <DNS />,
|
||||
component: DNS,
|
||||
},
|
||||
{
|
||||
path: "ping",
|
||||
name: "Ping",
|
||||
description: "Ping test tool",
|
||||
icon: <Activity />,
|
||||
component: <Ping />,
|
||||
component: Ping,
|
||||
},
|
||||
{
|
||||
path: "tcping",
|
||||
name: "TCPing",
|
||||
description: "TCP port connectivity test",
|
||||
icon: <Wifi />,
|
||||
component: <TCPing />,
|
||||
component: TCPing,
|
||||
},
|
||||
{
|
||||
path: "speedtest",
|
||||
name: "Speed Test",
|
||||
description: "Website speed test",
|
||||
icon: <Gauge />,
|
||||
component: <SpeedTest />,
|
||||
component: SpeedTest,
|
||||
},
|
||||
{
|
||||
path: "ipquery",
|
||||
name: "IP Query",
|
||||
description: "Query IP location, quality and risk info",
|
||||
icon: <MapPin />,
|
||||
component: <IPQuery />,
|
||||
component: IPQuery,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export { default as DNS } from './dns';
|
||||
export { default as Ping } from './ping';
|
||||
export { default as TCPing } from './tcping';
|
||||
export { default as SpeedTest } from './speedtest';
|
||||
export { default as IPQuery } from './ipquery';
|
||||
|
||||
@@ -91,8 +91,8 @@ const Tool: FC = () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
// 使用 ip-api.com (免费,功能较全)
|
||||
const response = await fetch(`http://ip-api.com/json/${encodeURIComponent(ip.trim())}?fields=status,message,country,countryCode,region,city,lat,lon,timezone,isp,org,as,proxy,hosting,query`);
|
||||
// 使用 ipinfo.io (免费,稳定可靠)
|
||||
const response = await fetch(`https://ipinfo.io/${encodeURIComponent(ip.trim())}/json`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
@@ -103,28 +103,8 @@ const Tool: FC = () => {
|
||||
|
||||
setQueryTime(endTime - startTime);
|
||||
|
||||
if (data.status === "fail") {
|
||||
toast.error(data.message || "Query failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换为统一格式
|
||||
const ipData: IPInfo = {
|
||||
ip: data.query,
|
||||
city: data.city,
|
||||
region: data.region,
|
||||
country: data.country,
|
||||
countryCode: data.countryCode,
|
||||
loc: data.lat && data.lon ? `${data.lat},${data.lon}` : undefined,
|
||||
timezone: data.timezone,
|
||||
isp: data.isp,
|
||||
org: data.org,
|
||||
as: data.as,
|
||||
proxy: data.proxy,
|
||||
hosting: data.hosting,
|
||||
};
|
||||
|
||||
setIpInfo(ipData);
|
||||
// ipinfo.io 返回格式已经符合 IPInfo 接口
|
||||
setIpInfo(data);
|
||||
toast.success("Query successful");
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -146,14 +126,18 @@ const Tool: FC = () => {
|
||||
const getRiskLevel = () => {
|
||||
if (!ipInfo) return null;
|
||||
|
||||
if (ipInfo.proxy || ipInfo.hosting) {
|
||||
// ipinfo.io 通过 org 字段可以简单判断是否为托管IP
|
||||
const orgLower = ipInfo.org?.toLowerCase() || "";
|
||||
const isHosting = orgLower.includes("hosting") ||
|
||||
orgLower.includes("datacenter") ||
|
||||
orgLower.includes("cloud") ||
|
||||
orgLower.includes("server");
|
||||
|
||||
if (isHosting) {
|
||||
return {
|
||||
level: "High",
|
||||
color: "text-red-500",
|
||||
reasons: [
|
||||
ipInfo.proxy && "Proxy/VPN detected",
|
||||
ipInfo.hosting && "Hosting/Datacenter IP",
|
||||
].filter(Boolean),
|
||||
level: "Medium",
|
||||
color: "text-yellow-500",
|
||||
reasons: ["Possible Hosting/Datacenter IP"],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,94 @@
|
||||
import { type FC } from "react";
|
||||
|
||||
import { type FC, useState } from "react";
|
||||
import { RefreshCw, Copy } from "lucide-react";
|
||||
import * as uuid from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface IDGeneratorProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onRegenerate: () => void;
|
||||
}
|
||||
|
||||
const IDGenerator: FC<IDGeneratorProps> = ({ label, value, onRegenerate }) => {
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast(`${label} has been copied to clipboard`);
|
||||
} catch (err) {
|
||||
toast.error("Copy failed");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="font-medium">{label}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex-1 px-3 py-2 bg-muted rounded-md font-mono text-sm break-all max-w-[400px]">
|
||||
{value}
|
||||
</span>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={onRegenerate}
|
||||
title="Regenerate"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={copyToClipboard}
|
||||
title="Copy"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Tool: FC = () => {
|
||||
const [uuidV1, setUuidV1] = useState(() => uuid.v1());
|
||||
const [uuidV4, setUuidV4] = useState(() => uuid.v4());
|
||||
const [uuidV6, setUuidV6] = useState(() => uuid.v6());
|
||||
const [uuidV7, setUuidV7] = useState(() => uuid.v7());
|
||||
const [nanoId, setNanoId] = useState(() => nanoid());
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-sm text-muted-foreground">Refresh the page to generate new UUID</span>
|
||||
<label>UUID Version 1</label>
|
||||
<span>{uuid.v1()}</span>
|
||||
<label>UUID Version 4</label>
|
||||
<span>{uuid.v4()}</span>
|
||||
<label>UUID Version 6</label>
|
||||
<span>{uuid.v6()}</span>
|
||||
<label>UUID Version 7</label>
|
||||
<span>{uuid.v7()}</span>
|
||||
<label>Nano ID</label>
|
||||
<span>{nanoid()}</span>
|
||||
<span className="text-sm text-muted-foreground">Click the refresh button to regenerate the corresponding ID</span>
|
||||
|
||||
<IDGenerator
|
||||
label="UUID Version 1"
|
||||
value={uuidV1}
|
||||
onRegenerate={() => setUuidV1(uuid.v1())}
|
||||
/>
|
||||
|
||||
<IDGenerator
|
||||
label="UUID Version 4"
|
||||
value={uuidV4}
|
||||
onRegenerate={() => setUuidV4(uuid.v4())}
|
||||
/>
|
||||
|
||||
<IDGenerator
|
||||
label="UUID Version 6"
|
||||
value={uuidV6}
|
||||
onRegenerate={() => setUuidV6(uuid.v6())}
|
||||
/>
|
||||
|
||||
<IDGenerator
|
||||
label="UUID Version 7"
|
||||
value={uuidV7}
|
||||
onRegenerate={() => setUuidV7(uuid.v7())}
|
||||
/>
|
||||
|
||||
<IDGenerator
|
||||
label="Nano ID"
|
||||
value={nanoId}
|
||||
onRegenerate={() => setNanoId(nanoid())}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
96
src/hooks/use-seo.ts
Normal file
96
src/hooks/use-seo.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
interface UseSEOOptions {
|
||||
title?: string;
|
||||
description?: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO Hook - 动态更新页面 SEO 元数据
|
||||
*
|
||||
* @param options - SEO 配置选项
|
||||
* @param options.title - 页面标题(可选)
|
||||
* @param options.description - 页面描述(可选)
|
||||
* @param options.baseUrl - 网站基础 URL,默认为 https://litek.typist.cc
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 在组件中使用
|
||||
* useSEO({
|
||||
* title: 'UUID Generator',
|
||||
* description: 'Free online UUID generator tool'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const useSEO = (options: UseSEOOptions = {}) => {
|
||||
const location = useLocation();
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
baseUrl = 'https://litek.typist.cc'
|
||||
} = options;
|
||||
|
||||
useEffect(() => {
|
||||
// 构建当前页面的完整 URL
|
||||
const canonicalUrl = `${baseUrl}${location.pathname}`;
|
||||
|
||||
// 更新或创建 canonical 链接
|
||||
let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement;
|
||||
if (!canonical) {
|
||||
canonical = document.createElement('link');
|
||||
canonical.setAttribute('rel', 'canonical');
|
||||
document.head.appendChild(canonical);
|
||||
}
|
||||
canonical.setAttribute('href', canonicalUrl);
|
||||
|
||||
// 更新页面标题
|
||||
if (title) {
|
||||
document.title = `${title} - Lite Kit`;
|
||||
}
|
||||
|
||||
// 更新 meta description
|
||||
if (description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]') as HTMLMetaElement;
|
||||
if (metaDescription) {
|
||||
metaDescription.setAttribute('content', description);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Open Graph URL
|
||||
let ogUrl = document.querySelector('meta[property="og:url"]') as HTMLMetaElement;
|
||||
if (ogUrl) {
|
||||
ogUrl.setAttribute('content', canonicalUrl);
|
||||
}
|
||||
|
||||
// 更新 Open Graph Title
|
||||
if (title) {
|
||||
let ogTitle = document.querySelector('meta[property="og:title"]') as HTMLMetaElement;
|
||||
if (ogTitle) {
|
||||
ogTitle.setAttribute('content', `${title} - Lite Kit`);
|
||||
}
|
||||
|
||||
// 更新 Twitter Card Title
|
||||
let twitterTitle = document.querySelector('meta[name="twitter:title"]') as HTMLMetaElement;
|
||||
if (twitterTitle) {
|
||||
twitterTitle.setAttribute('content', `${title} - Lite Kit`);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Open Graph Description
|
||||
if (description) {
|
||||
let ogDescription = document.querySelector('meta[property="og:description"]') as HTMLMetaElement;
|
||||
if (ogDescription) {
|
||||
ogDescription.setAttribute('content', description);
|
||||
}
|
||||
|
||||
// 更新 Twitter Card Description
|
||||
let twitterDescription = document.querySelector('meta[name="twitter:description"]') as HTMLMetaElement;
|
||||
if (twitterDescription) {
|
||||
twitterDescription.setAttribute('content', description);
|
||||
}
|
||||
}
|
||||
}, [location.pathname, title, description, baseUrl]);
|
||||
};
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'Roboto Mono', 'Noto Sans SC', 'SF Mono', Consolas, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,28 @@ import { Outlet } from "react-router-dom";
|
||||
import { ThemeProvider } from "@/components/theme/provider"
|
||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { AppSidebar } from "@/components/sidebar";
|
||||
import { ModeToggle } from "@/components/theme/toggle";
|
||||
|
||||
export const Layout: FC = () => (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<div className="p-4 flex flex-col w-full h-[100vh] overflow-hidden">
|
||||
<nav className="flex items-center justify-between">
|
||||
<SidebarTrigger className="size-10" />
|
||||
<div role="actions" />
|
||||
</nav>
|
||||
<main className="flex-1 overflow-auto p-4 overflow-hidden">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
import { useSEO } from "@/hooks/use-seo";
|
||||
|
||||
export const Layout: FC = () => {
|
||||
// 使用 SEO hook 自动更新 canonical URL 和其他 SEO 元数据
|
||||
useSEO();
|
||||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<div className="p-4 flex flex-col w-full h-[100vh] overflow-hidden">
|
||||
<nav className="flex items-center justify-between">
|
||||
<SidebarTrigger className="size-10" />
|
||||
<ModeToggle />
|
||||
</nav>
|
||||
<main className="flex-1 overflow-auto p-4 overflow-hidden">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
18
src/main.tsx
18
src/main.tsx
@@ -13,3 +13,21 @@ createRoot(document.getElementById('root')!).render(
|
||||
<Toaster />
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
// 注册 Service Worker
|
||||
if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
||||
import('workbox-window').then(({ Workbox }) => {
|
||||
const wb = new Workbox('/sw.js')
|
||||
|
||||
wb.addEventListener('installed', (event) => {
|
||||
if (event.isUpdate) {
|
||||
console.log('New service worker installed, reloading page...')
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
|
||||
wb.register()
|
||||
}).catch((error) => {
|
||||
console.error('Failed to register service worker:', error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Suspense, createElement } from "react";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
redirect,
|
||||
@@ -8,6 +9,19 @@ import {
|
||||
import { tools, type Tool } from "@/components/tool";
|
||||
import { Layout } from "./layout";
|
||||
|
||||
// 加载中的占位组件
|
||||
const LoadingFallback = () => (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="h-12 w-12 rounded-full border-4 border-muted"></div>
|
||||
<div className="absolute top-0 left-0 h-12 w-12 rounded-full border-4 border-primary border-t-transparent animate-spin"></div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-medium">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const buildToolRoutes = (tools: Tool[]): RouteObject[] => {
|
||||
return tools.map((tool) => {
|
||||
const route: RouteObject = {
|
||||
@@ -15,7 +29,12 @@ const buildToolRoutes = (tools: Tool[]): RouteObject[] => {
|
||||
};
|
||||
|
||||
if (tool.component) {
|
||||
route.element = tool.component;
|
||||
// 使用 Suspense 包裹懒加载组件
|
||||
route.element = (
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
{createElement(tool.component)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (tool.children && tool.children.length > 0) {
|
||||
|
||||
3
src/vite-env.d.ts
vendored
Normal file
3
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-pwa/client" />
|
||||
|
||||
121
vite.config.ts
121
vite.config.ts
@@ -2,13 +2,132 @@ import path from "path"
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['lite.svg', 'robots.txt', 'sitemap.xml'],
|
||||
manifest: {
|
||||
name: 'Lite Kit - Lightweight Online Tools',
|
||||
short_name: 'Lite Kit',
|
||||
description: 'Free online tools including UUID generator, JSON formatter, Base64 encoder/decoder, network testing tools and more',
|
||||
theme_color: '#000000',
|
||||
background_color: '#000000',
|
||||
display: 'standalone',
|
||||
icons: [
|
||||
{
|
||||
src: '/lite.svg',
|
||||
type: 'image/svg+xml',
|
||||
sizes: 'any'
|
||||
}
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /^https:\/\/ipinfo\.io\/.*/i,
|
||||
handler: "NetworkFirst",
|
||||
options: {
|
||||
cacheName: 'ipinfo-cache',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 // 延长到 1 小时
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Google Fonts 样式表缓存
|
||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||
handler: 'StaleWhileRevalidate',
|
||||
options: {
|
||||
cacheName: 'google-fonts-stylesheets',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365 // 1 年
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Google Fonts 字体文件缓存
|
||||
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'google-fonts-webfonts',
|
||||
expiration: {
|
||||
maxEntries: 30,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365 // 1 年
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
skipWaiting: true
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: (id) => {
|
||||
// React 核心拆分得更细
|
||||
if (id.includes('node_modules/react/') && !id.includes('node_modules/react-dom')) {
|
||||
return 'react-core';
|
||||
}
|
||||
if (id.includes('node_modules/react-dom/')) {
|
||||
return 'react-dom';
|
||||
}
|
||||
if (id.includes('node_modules/react-router-dom')) {
|
||||
return 'react-router';
|
||||
}
|
||||
// Radix UI组件
|
||||
if (id.includes('node_modules/@radix-ui')) {
|
||||
return 'ui-vendor';
|
||||
}
|
||||
// 图标库
|
||||
if (id.includes('node_modules/lucide-react')) {
|
||||
return 'icons';
|
||||
}
|
||||
// 其他工具库
|
||||
if (id.includes('node_modules/')) {
|
||||
return 'vendor';
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
// 启用更激进的压缩
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log'],
|
||||
// 移除未使用的代码
|
||||
unused: true,
|
||||
// 移除死代码
|
||||
dead_code: true,
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 500,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user