Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7646830194 | ||
|
|
59f998e8e3 | ||
|
|
55207beff5 | ||
|
|
e98a344b95 | ||
|
|
d553c3e04c | ||
|
|
e2da2758cc | ||
|
|
3ab70498e6 |
@@ -14,6 +14,10 @@
|
||||
<!-- 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>
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://litek.typist.cc/" />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "litek",
|
||||
"private": true,
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.19",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "npm run generate:sitemap && tsc -b && vite build",
|
||||
"build": "tsc -b && vite build",
|
||||
"generate:sitemap": "tsx scripts/generate-sitemap.ts",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4,6 +4,8 @@ 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";
|
||||
|
||||
import { useSEO } from "@/hooks/use-seo";
|
||||
|
||||
export const Layout: FC = () => {
|
||||
@@ -17,7 +19,7 @@ export const Layout: FC = () => {
|
||||
<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" />
|
||||
<ModeToggle />
|
||||
</nav>
|
||||
<main className="flex-1 overflow-auto p-4 overflow-hidden">
|
||||
<Outlet />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -11,4 +11,27 @@ export default defineConfig({
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: (id) => {
|
||||
// React核心库
|
||||
if (id.includes('node_modules/react') ||
|
||||
id.includes('node_modules/react-dom') ||
|
||||
id.includes('node_modules/react-router-dom')) {
|
||||
return 'react-vendor';
|
||||
}
|
||||
// Radix UI组件
|
||||
if (id.includes('node_modules/@radix-ui')) {
|
||||
return 'ui-vendor';
|
||||
}
|
||||
// 图标库
|
||||
if (id.includes('node_modules/lucide-react')) {
|
||||
return 'icons';
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 500,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user