Compare commits
	
		
			36 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 006f3d4dbb | ||
|   | ff8d497f97 | ||
|   | 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 | 
| @@ -43,8 +43,8 @@ jobs: | |||||||
|           push: true |           push: true | ||||||
|           tags: ${{ steps.meta.outputs.tags }} |           tags: ${{ steps.meta.outputs.tags }} | ||||||
|           labels: ${{ steps.meta.outputs.labels }} |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|           cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache |           # 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-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max | ||||||
|           # platforms: linux/amd64,linux/arm64 |           # platforms: linux/amd64,linux/arm64 | ||||||
|           platforms: linux/amd64 |           platforms: linux/amd64 | ||||||
|        |        | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								index.html
									
									
									
									
									
								
							| @@ -2,12 +2,71 @@ | |||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="UTF-8" /> |     <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" /> |     <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> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <div id="root"></div> |     <div id="root"></div> | ||||||
|     <script type="module" src="/src/main.tsx"></script> |     <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> |   </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,11 +1,12 @@ | |||||||
| { | { | ||||||
|   "name": "litek", |   "name": "litek", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "version": "0.0.12", |   "version": "0.0.28", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "vite", |     "dev": "vite", | ||||||
|     "build": "tsc -b && vite build", |     "build": "tsc -b && vite build", | ||||||
|  |     "generate:sitemap": "tsx scripts/generate-sitemap.ts", | ||||||
|     "lint": "eslint .", |     "lint": "eslint .", | ||||||
|     "preview": "vite preview", |     "preview": "vite preview", | ||||||
|     "release:patch": "npm version patch && git push --follow-tags", |     "release:patch": "npm version patch && git push --follow-tags", | ||||||
| @@ -43,17 +44,22 @@ | |||||||
|     "eslint-plugin-react-hooks": "^5.2.0", |     "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|     "eslint-plugin-react-refresh": "^0.4.22", |     "eslint-plugin-react-refresh": "^0.4.22", | ||||||
|     "globals": "^16.4.0", |     "globals": "^16.4.0", | ||||||
|  |     "terser": "^5.44.0", | ||||||
|  |     "tsx": "^4.19.2", | ||||||
|     "tw-animate-css": "^1.4.0", |     "tw-animate-css": "^1.4.0", | ||||||
|     "typescript": "~5.9.3", |     "typescript": "~5.9.3", | ||||||
|     "typescript-eslint": "^8.45.0", |     "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": { |   "pnpm": { | ||||||
|     "overrides": { |     "overrides": { | ||||||
|       "vite": "npm:rolldown-vite@7.1.14" |       "vite": "npm:rolldown-vite@7.1.14" | ||||||
|     }, |     }, | ||||||
|     "onlyBuiltDependencies": [ |     "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
											
										
									
								
							
							
								
								
									
										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 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 { 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 { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; | ||||||
| import { tools, type Tool } from "@/components/tool"; | 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 = () => { | export const AppSidebar = () => { | ||||||
|   // 递归构建完整路径 |   // 递归构建完整路径 | ||||||
| @@ -13,6 +13,46 @@ export const AppSidebar = () => { | |||||||
|     return `/tool/${pathSegments.join("/")}`; |     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 renderMenuItem = (tool: Tool, parentPaths: string[] = []): ReactNode => { | ||||||
|     const currentPaths = [...parentPaths, tool.path]; |     const currentPaths = [...parentPaths, tool.path]; | ||||||
| @@ -20,12 +60,11 @@ export const AppSidebar = () => { | |||||||
|     if (tool.children) { |     if (tool.children) { | ||||||
|       // 有子菜单的项目 |       // 有子菜单的项目 | ||||||
|       return ( |       return ( | ||||||
|         <Collapsible |         <SidebarMenuItem key={tool.name}> | ||||||
|           key={tool.name} |           <Collapsible | ||||||
|           defaultOpen={false} |             defaultOpen={false} | ||||||
|           className="group/collapsible" |             className="group/collapsible" | ||||||
|         > |           > | ||||||
|           <SidebarMenuItem> |  | ||||||
|             <CollapsibleTrigger asChild> |             <CollapsibleTrigger asChild> | ||||||
|               <SidebarMenuButton tooltip={tool.description}> |               <SidebarMenuButton tooltip={tool.description}> | ||||||
|                 {tool.icon} |                 {tool.icon} | ||||||
| @@ -37,25 +76,13 @@ export const AppSidebar = () => { | |||||||
|               <SidebarMenuSub> |               <SidebarMenuSub> | ||||||
|                 {tool.children.map((child) => ( |                 {tool.children.map((child) => ( | ||||||
|                   <SidebarMenuSubItem key={child.name}> |                   <SidebarMenuSubItem key={child.name}> | ||||||
|                     {child.children ? ( |                     {renderSubMenuContent(child, currentPaths)} | ||||||
|                       renderMenuItem(child, currentPaths) |  | ||||||
|                     ) : ( |  | ||||||
|                       <SidebarMenuSubButton asChild> |  | ||||||
|                         <Link |  | ||||||
|                           to={buildFullPath([...currentPaths, child.path])} |  | ||||||
|                           title={child.description} |  | ||||||
|                         > |  | ||||||
|                           {child.icon} |  | ||||||
|                           <span>{child.name}</span> |  | ||||||
|                         </Link> |  | ||||||
|                       </SidebarMenuSubButton> |  | ||||||
|                     )} |  | ||||||
|                   </SidebarMenuSubItem> |                   </SidebarMenuSubItem> | ||||||
|                 ))} |                 ))} | ||||||
|               </SidebarMenuSub> |               </SidebarMenuSub> | ||||||
|             </CollapsibleContent> |             </CollapsibleContent> | ||||||
|           </SidebarMenuItem> |           </Collapsible> | ||||||
|         </Collapsible> |         </SidebarMenuItem> | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -87,12 +114,7 @@ export const AppSidebar = () => { | |||||||
|           </SidebarGroupContent> |           </SidebarGroupContent> | ||||||
|         </SidebarGroup> |         </SidebarGroup> | ||||||
|       </SidebarContent> |       </SidebarContent> | ||||||
|       <SidebarFooter className="flex flex-row justify-between items-center gap-2"> |       <SidebarFooter className="flex flex-col gap-2" /> | ||||||
|         <Button variant="link"> |  | ||||||
|           <a href="mailto:litek@mail.typist.cc">need more tools?</a> |  | ||||||
|         </Button> |  | ||||||
|         <ModeToggle /> |  | ||||||
|       </SidebarFooter> |  | ||||||
|     </Sidebar> |     </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 { FileJson, Hash, Binary, Network, Globe, Activity, Gauge, Wifi, MapPin } from 'lucide-react' | ||||||
|  |  | ||||||
| import UUID from './uuid' | // 懒加载工具组件 | ||||||
| import JSON from './json' | const UUID = lazy(() => import('./uuid')) | ||||||
| import Base64 from './base64' | const JSON = lazy(() => import('./json')) | ||||||
| import { DNS, Ping, TCPing, SpeedTest, IPQuery } from './network' | 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 { | export interface Tool { | ||||||
|   path: string; |   path: string; | ||||||
|   name: string; |   name: string; | ||||||
|   icon: ReactNode; |   icon: ReactNode; | ||||||
|   description: string; |   description: string; | ||||||
|   component?: ReactNode; |   component?: ComponentType; | ||||||
|   children?: Tool[]; |   children?: Tool[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -21,21 +26,21 @@ export const tools: Tool[] = [ | |||||||
|     name: "UUID Generator", |     name: "UUID Generator", | ||||||
|     description: "Generate a UUID", |     description: "Generate a UUID", | ||||||
|     icon: <Hash />, |     icon: <Hash />, | ||||||
|     component: <UUID />, |     component: UUID, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "json", |     path: "json", | ||||||
|     name: "JSON Formatter", |     name: "JSON Formatter", | ||||||
|     description: "Format and validate JSON", |     description: "Format and validate JSON", | ||||||
|     icon: <FileJson />, |     icon: <FileJson />, | ||||||
|     component: <JSON />, |     component: JSON, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "base64", |     path: "base64", | ||||||
|     name: "Base64 Encoder/Decoder", |     name: "Base64 Encoder/Decoder", | ||||||
|     description: "Encode and decode Base64", |     description: "Encode and decode Base64", | ||||||
|     icon: <Binary />, |     icon: <Binary />, | ||||||
|     component: <Base64 />, |     component: Base64, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "network", |     path: "network", | ||||||
| @@ -48,35 +53,35 @@ export const tools: Tool[] = [ | |||||||
|         name: "DNS Lookup", |         name: "DNS Lookup", | ||||||
|         description: "DNS query tool", |         description: "DNS query tool", | ||||||
|         icon: <Globe />, |         icon: <Globe />, | ||||||
|         component: <DNS />, |         component: DNS, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         path: "ping", |         path: "ping", | ||||||
|         name: "Ping", |         name: "Ping", | ||||||
|         description: "Ping test tool", |         description: "Ping test tool", | ||||||
|         icon: <Activity />, |         icon: <Activity />, | ||||||
|         component: <Ping />, |         component: Ping, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         path: "tcping", |         path: "tcping", | ||||||
|         name: "TCPing", |         name: "TCPing", | ||||||
|         description: "TCP port connectivity test", |         description: "TCP port connectivity test", | ||||||
|         icon: <Wifi />, |         icon: <Wifi />, | ||||||
|         component: <TCPing />, |         component: TCPing, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         path: "speedtest", |         path: "speedtest", | ||||||
|         name: "Speed Test", |         name: "Speed Test", | ||||||
|         description: "Website speed test", |         description: "Website speed test", | ||||||
|         icon: <Gauge />, |         icon: <Gauge />, | ||||||
|         component: <SpeedTest />, |         component: SpeedTest, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         path: "ipquery", |         path: "ipquery", | ||||||
|         name: "IP Query", |         name: "IP Query", | ||||||
|         description: "Query IP location, quality and risk info", |         description: "Query IP location, quality and risk info", | ||||||
|         icon: <MapPin />, |         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(); |     const startTime = performance.now(); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       // 使用 ip-api.com (免费,功能较全) |       // 使用 ipinfo.io (免费,稳定可靠) | ||||||
|       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`); |       const response = await fetch(`https://ipinfo.io/${encodeURIComponent(ip.trim())}/json`); | ||||||
|        |        | ||||||
|       if (!response.ok) { |       if (!response.ok) { | ||||||
|         throw new Error(`HTTP error! status: ${response.status}`); |         throw new Error(`HTTP error! status: ${response.status}`); | ||||||
| @@ -103,28 +103,8 @@ const Tool: FC = () => { | |||||||
|        |        | ||||||
|       setQueryTime(endTime - startTime); |       setQueryTime(endTime - startTime); | ||||||
|  |  | ||||||
|       if (data.status === "fail") { |       // ipinfo.io 返回格式已经符合 IPInfo 接口 | ||||||
|         toast.error(data.message || "Query failed"); |       setIpInfo(data); | ||||||
|         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); |  | ||||||
|       toast.success("Query successful"); |       toast.success("Query successful"); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       if (error instanceof Error) { |       if (error instanceof Error) { | ||||||
| @@ -146,14 +126,18 @@ const Tool: FC = () => { | |||||||
|   const getRiskLevel = () => { |   const getRiskLevel = () => { | ||||||
|     if (!ipInfo) return null; |     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 { |       return { | ||||||
|         level: "High", |         level: "Medium", | ||||||
|         color: "text-red-500", |         color: "text-yellow-500", | ||||||
|         reasons: [ |         reasons: ["Possible Hosting/Datacenter IP"], | ||||||
|           ipInfo.proxy && "Proxy/VPN detected", |  | ||||||
|           ipInfo.hosting && "Hosting/Datacenter IP", |  | ||||||
|         ].filter(Boolean), |  | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -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 * as uuid from 'uuid' | ||||||
| import { nanoid } from 'nanoid' | 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 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 ( |   return ( | ||||||
|     <div className="flex flex-col gap-4"> |     <div className="flex flex-col gap-4"> | ||||||
|       <span className="text-sm text-muted-foreground">Refresh the page to generate new UUID</span> |       <span className="text-sm text-muted-foreground">Click the refresh button to regenerate the corresponding ID</span> | ||||||
|       <label>UUID Version 1</label> |        | ||||||
|       <span>{uuid.v1()}</span> |       <IDGenerator  | ||||||
|       <label>UUID Version 4</label> |         label="UUID Version 1"  | ||||||
|       <span>{uuid.v4()}</span> |         value={uuidV1}  | ||||||
|       <label>UUID Version 6</label> |         onRegenerate={() => setUuidV1(uuid.v1())}  | ||||||
|       <span>{uuid.v6()}</span> |       /> | ||||||
|       <label>UUID Version 7</label> |        | ||||||
|       <span>{uuid.v7()}</span> |       <IDGenerator  | ||||||
|       <label>Nano ID</label> |         label="UUID Version 4"  | ||||||
|       <span>{nanoid()}</span> |         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> |     </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 { |   body { | ||||||
|     @apply bg-background text-foreground; |     @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 { ThemeProvider } from "@/components/theme/provider" | ||||||
| import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" | import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" | ||||||
| import { AppSidebar } from "@/components/sidebar"; | import { AppSidebar } from "@/components/sidebar"; | ||||||
|  | import { ModeToggle } from "@/components/theme/toggle"; | ||||||
|  |  | ||||||
| export const Layout: FC = () => ( | import { useSEO } from "@/hooks/use-seo"; | ||||||
|   <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> |  | ||||||
|     <SidebarProvider> | export const Layout: FC = () => { | ||||||
|       <AppSidebar /> |   // 使用 SEO hook 自动更新 canonical URL 和其他 SEO 元数据 | ||||||
|       <div className="p-4 flex flex-col w-full h-[100vh] overflow-hidden"> |   useSEO(); | ||||||
|         <nav className="flex items-center justify-between"> |  | ||||||
|           <SidebarTrigger className="size-10" /> |   return ( | ||||||
|           <div role="actions" /> |     <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> | ||||||
|         </nav> |       <SidebarProvider> | ||||||
|         <main className="flex-1 overflow-auto p-4 overflow-hidden"> |         <AppSidebar /> | ||||||
|           <Outlet /> |         <div className="p-4 flex flex-col w-full h-[100vh] overflow-hidden"> | ||||||
|         </main> |           <nav className="flex items-center justify-between"> | ||||||
|       </div> |             <SidebarTrigger className="size-10" /> | ||||||
|     </SidebarProvider> |             <ModeToggle /> | ||||||
|   </ThemeProvider> |           </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 /> |     <Toaster /> | ||||||
|   </StrictMode> |   </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 { | import { | ||||||
|   createBrowserRouter, |   createBrowserRouter, | ||||||
|   redirect, |   redirect, | ||||||
| @@ -8,6 +9,19 @@ import { | |||||||
| import { tools, type Tool } from "@/components/tool"; | import { tools, type Tool } from "@/components/tool"; | ||||||
| import { Layout } from "./layout"; | 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[] => { | const buildToolRoutes = (tools: Tool[]): RouteObject[] => { | ||||||
|   return tools.map((tool) => { |   return tools.map((tool) => { | ||||||
|     const route: RouteObject = { |     const route: RouteObject = { | ||||||
| @@ -15,7 +29,12 @@ const buildToolRoutes = (tools: Tool[]): RouteObject[] => { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (tool.component) { |     if (tool.component) { | ||||||
|       route.element = tool.component; |       // 使用 Suspense 包裹懒加载组件 | ||||||
|  |       route.element = ( | ||||||
|  |         <Suspense fallback={<LoadingFallback />}> | ||||||
|  |           {createElement(tool.component)} | ||||||
|  |         </Suspense> | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (tool.children && tool.children.length > 0) { |     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 { defineConfig } from 'vite' | ||||||
| import react from '@vitejs/plugin-react' | import react from '@vitejs/plugin-react' | ||||||
| import tailwindcss from "@tailwindcss/vite" | import tailwindcss from "@tailwindcss/vite" | ||||||
|  | import { VitePWA } from 'vite-plugin-pwa' | ||||||
|  |  | ||||||
| // https://vite.dev/config/ | // https://vite.dev/config/ | ||||||
| export default defineConfig({ | 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: { |   resolve: { | ||||||
|     alias: { |     alias: { | ||||||
|       "@": path.resolve(__dirname, "./src"), |       "@": 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