react+ts+vite+tailwind+shadcn

1.创建项目

复制代码
yarn create vite my-react-app --template react-ts

遇到的问题:

复制代码
error create-vite@8.0.2: The engine "node" is incompatible with this module. Expected version "^20.19.0 || >=22.12.0". Got "18.18.0"

升级node

复制代码
nvm install 22.12.0

切换node并安装yarn后重新执行创建

复制代码
nvm use 22.12.0

npm install -g yarn

遇到的选择:实验性说明还不够稳定,所以选择NO

复制代码
 Use rolldown-vite (Experimental)?:
|  No
|
o  Install with yarn and start now?
|  Yes

安装依赖并启动成功

复制代码
  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

访问端口

安装依赖包

复制代码
# 状态管理
yarn add zustand

# 路由
yarn add react-router-dom
yarn add -D @types/react-router-dom

# HTTP 客户端
yarn add axios

# 工具库
yarn add clsx tailwind-merge

# 开发工具
yarn add -D eslint prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react-hooks eslint-plugin-react-refresh eslint-config-prettier

配置 Tailwind CSS + Shadcn/ui

复制代码
# 安装 Tailwind CSS
yarn add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

遇到的问题:npx tailwindcss init -p报错

安装低版本的解决

复制代码
yarn add -D tailwindcss@3.4.17

配置 tailwind.config.js

复制代码
/** @type {import('tailwindcss').Config} */
export default {
  darkMode: ["class"],
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
      },
    },
  },
  plugins: []
}

配置tsconfig.json

复制代码
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": ["node"]
  }
}

# 初始化 Shadcn/ui
npx shadcn@latest init

选择base color如果没有想要的,先选择一个再访问官方shadcn 复制代码修改index.css,比如修改为blue,先点击个性化,选中blue,点击复制代码,覆盖index.css中的样式

复制代码
// index.css 蓝色主题

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#root {
  width: 100%;
}
a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
  overflow: hidden;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}





@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 221.2 83.2% 53.3%;
    --radius: 0.5rem;
    --chart-1: 12 76% 61%;
    --chart-2: 173 58% 39%;
    --chart-3: 197 37% 24%;
    --chart-4: 43 74% 66%;
    --chart-5: 27 87% 67%;
    --sidebar-background: 0 0% 98%;
    --sidebar-foreground: 240 5.3% 26.1%;
    --sidebar-primary: 240 5.9% 10%;
    --sidebar-primary-foreground: 0 0% 98%;
    --sidebar-accent: 240 4.8% 95.9%;
    --sidebar-accent-foreground: 240 5.9% 10%;
    --sidebar-border: 220 13% 91%;
    --sidebar-ring: 217.2 91.2% 59.8%;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 224.3 76.3% 48%;
    --chart-1: 220 70% 50%;
    --chart-2: 160 60% 45%;
    --chart-3: 30 80% 55%;
    --chart-4: 280 65% 60%;
    --chart-5: 340 75% 55%;
    --sidebar-background: 240 5.9% 10%;
    --sidebar-foreground: 240 4.8% 95.9%;
    --sidebar-primary: 224.3 76.3% 48%;
    --sidebar-primary-foreground: 0 0% 100%;
    --sidebar-accent: 240 3.7% 15.9%;
    --sidebar-accent-foreground: 240 4.8% 95.9%;
    --sidebar-border: 240 3.7% 15.9%;
    --sidebar-ring: 217.2 91.2% 59.8%;
  }
}



@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

配置路径别名

复制代码
// vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

安装类型定义

复制代码
yarn add -D @types/node

配置 ESLint 和 Prettier

复制代码
// 创建 .eslintrc.js:

module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
    'eslint-config-prettier'
  ],
  ignorePatterns: ['dist', '.eslintrc.js'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh', '@typescript-eslint'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-explicit-any': 'warn',
  },
}


// 创建 .prettierrc:


{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "es5",
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false,
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "endOfLine": "lf"
}

// 创建 .prettierignore:


node_modules
dist
build
.coverage
.eslintrc.js
*.md

// 更新 package.json 脚本:


{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "lint:fix": "eslint . --ext ts,tsx --fix",
    "prettier": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
    "prettier:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
    "format": "yarn prettier && yarn lint:fix"
  }
}

配置路由

复制代码
// router/index.tsx

import { createBrowserRouter } from 'react-router-dom'
import Layout from '@/components/Layout'
import ErrorBoundary from '@/components/ErrorBoundary'
import Home from '@/pages/Home'
import About from '@/pages/About'
import Users from '@/pages/Users'
import UserDetail from '@/pages/UserDetail'
import NotFound from '@/pages/NotFound'
import { userService } from '@/api/userService'

export interface RouteLoaderData {
    users?: any[]
    user?: any
}

export const router = createBrowserRouter([
    {
        path: '/',
        element: <Layout />,
        errorElement: <ErrorBoundary />,
        children: [
            {
                index: true,
                element: <Home />,
            },
            {
                path: 'about',
                element: <About />,
            },
            {
                path: 'users',
                children: [
                    {
                        index: true,
                        element: <Users />,
                        loader: async (): Promise<RouteLoaderData> => {
                            const users = await userService.getUsers()
                            return { users }
                        },
                    },
                    {
                        path: ':userId',
                        element: <UserDetail />,
                        loader: async ({ params }): Promise<RouteLoaderData> => {
                            const user = await userService.getUserById(Number(params.userId))
                            return { user }
                        },
                    },
                ],
            },
            {
                path: '*',
                element: <NotFound />,
            },
        ],
    },
])

main.tsx

复制代码
// main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { router } from './router'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
        <RouterProvider router={router} />
    </React.StrictMode>,
)

页面中用到的组件需要安装

npx shadcn@latest add button input sidebar card badge

/components/Layout.tsx

复制代码
// Layout.tsx

import { Outlet, Link, useNavigation } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"
import { cn } from '@/lib/utils'

export default function Layout() {
    const navigation = useNavigation()

    return (
        <div className="min-h-screen bg-background">
            <nav className="border-b bg-card">
                <div className=" px-4 py-3 flex items-center justify-between">
                    <h1 className="text-xl font-semibold">My React App</h1>
                    <div className="flex space-x-2">
                        <Link to="/">
                            <Button variant="ghost">Home</Button>
                        </Link>
                        <Link to="/about">
                            <Button variant="ghost">About</Button>
                        </Link>
                        <Link to="/users">
                            <Button variant="ghost">Users</Button>
                        </Link>
                    </div>
                </div>
            </nav>
            <SidebarProvider className="sidebar-custom">
                <AppSidebar />
                <main className={cn(
                    "w-full px-4 py-8 main-custom",
                    navigation.state === 'loading' && 'opacity-50 pointer-events-none'
                )}>
                    {/*<SidebarTrigger />*/}
                    {navigation.state === 'loading' && (
                        <div className="fixed top-4 right-4 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg">
                            Loading...
                        </div>
                    )}
                    <Outlet />
                </main>
            </SidebarProvider>
        </div>
    )
}

/pages/Home.tsx

复制代码
// Home.tsx

import { Link } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { useCounterStore } from '@/stores/counterStore'

export default function Home() {
    const { count, increase, decrease, reset } = useCounterStore()

    return (
        <div className="max-w-6xl space-y-8">
            {/* Hero Section */}
            <section className="text-center space-y-4">
                <h1 className="text-4xl md:text-6xl font-bold tracking-tight">
                    欢迎使用
                    <span className="text-primary block mt-2">现代化 React 项目</span>
                </h1>
                <p className="text-xl text-muted-foreground max-w-2xl mx-auto">
                    基于 React 18 + TypeScript + Vite + 现代化工具链构建的完整前端解决方案
                </p>
                <div className="flex gap-4 justify-center pt-4">
                    <Link to="/users">
                        <Button size="lg">探索用户列表</Button>
                    </Link>
                    <Link to="/about">
                        <Button size="lg" variant="outline">
                            了解更多
                        </Button>
                    </Link>
                </div>
            </section>

            {/* Features Grid */}
            <section className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 pt-8">
                <Card>
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <div className="w-2 h-2 bg-blue-500 rounded-full" />
                            React 18
                        </CardTitle>
                        <CardDescription>最新版本的 React,支持并发特性</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <p className="text-sm">使用 Hooks、Suspense 等现代 React 特性</p>
                    </CardContent>
                </Card>

                <Card>
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <div className="w-2 h-2 bg-green-500 rounded-full" />
                            TypeScript
                        </CardTitle>
                        <CardDescription>完整的类型安全支持</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <p className="text-sm">严格的类型检查,更好的开发体验</p>
                    </CardContent>
                </Card>

                <Card>
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <div className="w-2 h-2 bg-purple-500 rounded-full" />
                            Vite
                        </CardTitle>
                        <CardDescription>极速的开发服务器</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <p className="text-sm">快速的冷启动和热更新</p>
                    </CardContent>
                </Card>

                <Card>
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <div className="w-2 h-2 bg-orange-500 rounded-full" />
                            Zustand
                        </CardTitle>
                        <CardDescription>轻量级状态管理</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <p className="text-sm">简单易用的状态管理解决方案</p>
                    </CardContent>
                </Card>

                <Card>
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <div className="w-2 h-2 bg-red-500 rounded-full" />
                            Tailwind CSS
                        </CardTitle>
                        <CardDescription>实用优先的 CSS 框架</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <p className="text-sm">快速构建自定义设计</p>
                    </CardContent>
                </Card>

                <Card>
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <div className="w-2 h-2 bg-indigo-500 rounded-full" />
                            Shadcn/ui
                        </CardTitle>
                        <CardDescription>可复用的 UI 组件</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <p className="text-sm">基于 Radix UI 的高质量组件</p>
                    </CardContent>
                </Card>
            </section>

            {/* Zustand 示例 */}
            <section className="pt-8">
                <Card>
                    <CardHeader>
                        <CardTitle>Zustand 状态管理示例</CardTitle>
                        <CardDescription>
                            演示如何使用 Zustand 进行全局状态管理
                        </CardDescription>
                    </CardHeader>
                    <CardContent className="space-y-4">
                        <div className="flex items-center justify-between p-4 bg-muted rounded-lg">
                            <span className="text-lg font-semibold">计数器:</span>
                            <span className="text-2xl font-bold text-primary">{count}</span>
                        </div>
                        <div className="flex gap-2 flex-wrap">
                            <Button onClick={increase} variant="default">
                                增加 +
                            </Button>
                            <Button onClick={decrease} variant="outline">
                                减少 -
                            </Button>
                            <Button onClick={reset} variant="secondary">
                                重置
                            </Button>
                        </div>
                    </CardContent>
                </Card>
            </section>

            {/* Quick Actions */}
            <section className="pt-8">
                <Card>
                    <CardHeader>
                        <CardTitle>快速导航</CardTitle>
                        <CardDescription>探索项目的不同功能</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
                            <Link to="/users">
                                <Card className="cursor-pointer transition-all hover:shadow-md">
                                    <CardContent className="p-6 text-center">
                                        <div className="text-2xl mb-2">👥</div>
                                        <h3 className="font-semibold">用户管理</h3>
                                        <p className="text-sm text-muted-foreground mt-1">
                                            查看用户列表和详情
                                        </p>
                                    </CardContent>
                                </Card>
                            </Link>
                            <Link to="/about">
                                <Card className="cursor-pointer transition-all hover:shadow-md">
                                    <CardContent className="p-6 text-center">
                                        <div className="text-2xl mb-2">ℹ️</div>
                                        <h3 className="font-semibold">项目信息</h3>
                                        <p className="text-sm text-muted-foreground mt-1">
                                            了解技术栈和特性
                                        </p>
                                    </CardContent>
                                </Card>
                            </Link>
                            <Card className="cursor-pointer transition-all hover:shadow-md bg-muted/50">
                                <CardContent className="p-6 text-center">
                                    <div className="text-2xl mb-2">🚀</div>
                                    <h3 className="font-semibold">更多功能</h3>
                                    <p className="text-sm text-muted-foreground mt-1">
                                        持续开发中...
                                    </p>
                                </CardContent>
                            </Card>
                        </div>
                    </CardContent>
                </Card>
            </section>
        </div>
    )
}

/pages/About.tsx

复制代码
// About.tsx

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'

const technologies = [
    {
        category: "前端框架",
        items: [
            { name: "React 18", description: "用于构建用户界面的 JavaScript 库", version: "18.x" },
            { name: "TypeScript", description: "JavaScript 的超集,提供类型安全", version: "5.x" },
        ]
    },
    {
        category: "构建工具",
        items: [
            { name: "Vite", description: "下一代前端构建工具", version: "4.x" },
            { name: "Yarn", description: "快速、可靠、安全的依赖管理", version: "Berry" },
        ]
    },
    {
        category: "状态管理",
        items: [
            { name: "Zustand", description: "轻量级状态管理解决方案", version: "4.x" },
        ]
    },
    {
        category: "路由",
        items: [
            { name: "React Router", description: "声明式路由管理", version: "6.x" },
        ]
    },
    {
        category: "UI & 样式",
        items: [
            { name: "Tailwind CSS", description: "实用优先的 CSS 框架", version: "3.x" },
            { name: "Shadcn/ui", description: "可复用的 UI 组件库", version: "Latest" },
        ]
    },
    {
        category: "开发工具",
        items: [
            { name: "ESLint", description: "代码质量检查工具", version: "8.x" },
            { name: "Prettier", description: "代码格式化工具", version: "3.x" },
            { name: "Axios", description: "HTTP 客户端", version: "1.x" },
        ]
    }
]

export default function About() {
    return (
        <div className="max-w-6xl space-y-8">
            {/* Header */}
            <div className="text-center space-y-4">
                <h1 className="text-4xl font-bold tracking-tight">关于项目</h1>
                <p className="text-xl text-muted-foreground max-w-3xl mx-auto">
                    一个现代化的 React 前端项目模板,集成了当前最流行的开发工具和最佳实践
                </p>
            </div>

            {/* Project Overview */}
            <Card>
                <CardHeader>
                    <CardTitle>项目概述</CardTitle>
                    <CardDescription>
                        这个项目展示了如何将现代前端工具链组合在一起,创建一个类型安全、可维护且开发体验优秀的应用
                    </CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                    <div className="grid md:grid-cols-2 gap-4">
                        <div>
                            <h3 className="font-semibold mb-2">主要特性</h3>
                            <ul className="space-y-2 text-sm">
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
                                    完整的 TypeScript 支持
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
                                    热重载和快速构建
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
                                    现代化的状态管理
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
                                    类型安全的路由
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
                                    统一的代码规范
                                </li>
                            </ul>
                        </div>
                        <div>
                            <h3 className="font-semibold mb-2">开发体验</h3>
                            <ul className="space-y-2 text-sm">
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-blue-500 rounded-full" />
                                    智能代码补全
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-blue-500 rounded-full" />
                                    实时错误检查
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-blue-500 rounded-full" />
                                    自动代码格式化
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-blue-500 rounded-full" />
                                    路径别名支持
                                </li>
                                <li className="flex items-center gap-2">
                                    <div className="w-1.5 h-1.5 bg-blue-500 rounded-full" />
                                    响应式设计
                                </li>
                            </ul>
                        </div>
                    </div>
                </CardContent>
            </Card>

            {/* Technology Stack */}
            <div className="space-y-6">
                <h2 className="text-2xl font-bold text-center">技术栈</h2>
                <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
                    {technologies.map((techGroup) => (
                        <Card key={techGroup.category}>
                            <CardHeader>
                                <CardTitle className="text-lg">{techGroup.category}</CardTitle>
                            </CardHeader>
                            <CardContent className="space-y-3">
                                {techGroup.items.map((tech) => (
                                    <div key={tech.name} className="space-y-1">
                                        <div className="flex items-center justify-between">
                                            <span className="font-medium">{tech.name}</span>
                                            <Badge variant="secondary" className="text-xs">
                                                v{tech.version}
                                            </Badge>
                                        </div>
                                        <p className="text-sm text-muted-foreground">
                                            {tech.description}
                                        </p>
                                    </div>
                                ))}
                            </CardContent>
                        </Card>
                    ))}
                </div>
            </div>

            {/* Project Structure */}
            <Card>
                <CardHeader>
                    <CardTitle>项目结构</CardTitle>
                    <CardDescription>
                        清晰的文件组织方式,便于维护和扩展
                    </CardDescription>
                </CardHeader>
                <CardContent>
                    <div className="bg-muted p-4 rounded-lg font-mono text-sm">
                        <div>src/</div>
                        <div className="ml-4">├── api/ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# API 服务层</div>
                        <div className="ml-4">├── components/ &nbsp;# 可复用组件</div>
                        <div className="ml-8">├── ui/ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Shadcn UI 组件</div>
                        <div className="ml-4">├── pages/ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# 页面组件</div>
                        <div className="ml-4">├── stores/ &nbsp;&nbsp;&nbsp;&nbsp;# 状态管理</div>
                        <div className="ml-4">├── hooks/ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# 自定义 Hooks</div>
                        <div className="ml-4">├── lib/ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# 工具函数</div>
                        <div className="ml-4">└── router/ &nbsp;&nbsp;&nbsp;&nbsp;# 路由配置</div>
                    </div>
                </CardContent>
            </Card>
        </div>
    )
}

/pages/NotFound.tsx

复制代码
// NotFound.tsx

import { Link, useNavigate } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Home, ArrowLeft, Search, AlertTriangle } from 'lucide-react'

export default function NotFound() {
    const navigate = useNavigate()

    return (
        <div className="min-h-[80vh] flex items-center justify-center p-4">
            <div className="max-w-md w-full space-y-6">
                {/* Error Illustration */}
                <div className="text-center space-y-4">
                    <div className="relative inline-block">
                        <div className="w-20 h-20 bg-muted rounded-full flex items-center justify-center mx-auto mb-4">
                            <AlertTriangle className="h-10 w-10 text-destructive" />
                        </div>
                    </div>

                    <div className="space-y-2">
                        <h1 className="text-4xl font-bold tracking-tight">404</h1>
                        <h2 className="text-2xl font-semibold">页面未找到</h2>
                        <p className="text-muted-foreground text-lg">
                            抱歉,您访问的页面不存在或已被移动
                        </p>
                    </div>
                </div>

                {/* Suggestions */}
                <Card>
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <Search className="h-5 w-5" />
                            可能的原因
                        </CardTitle>
                        <CardDescription>以下是一些可能导致此错误的原因</CardDescription>
                    </CardHeader>
                    <CardContent>
                        <ul className="space-y-2 text-sm">
                            <li className="flex items-start gap-2">
                                <div className="w-1.5 h-1.5 bg-muted-foreground rounded-full mt-1.5 flex-shrink-0" />
                                <span>URL 地址拼写错误</span>
                            </li>
                            <li className="flex items-start gap-2">
                                <div className="w-1.5 h-1.5 bg-muted-foreground rounded-full mt-1.5 flex-shrink-0" />
                                <span>页面已被删除或移动</span>
                            </li>
                            <li className="flex items-start gap-2">
                                <div className="w-1.5 h-1.5 bg-muted-foreground rounded-full mt-1.5 flex-shrink-0" />
                                <span>链接已过期</span>
                            </li>
                            <li className="flex items-start gap-2">
                                <div className="w-1.5 h-1.5 bg-muted-foreground rounded-full mt-1.5 flex-shrink-0" />
                                <span>没有访问权限</span>
                            </li>
                        </ul>
                    </CardContent>
                </Card>

                {/* Action Buttons */}
                <div className="flex flex-col sm:flex-row gap-3">
                    <Button onClick={() => navigate(-1)} variant="outline" className="flex-1">
                        <ArrowLeft className="h-4 w-4 mr-2" />
                        返回上页
                    </Button>
                    <Link to="/" className="flex-1">
                        <Button className="w-full">
                            <Home className="h-4 w-4 mr-2" />
                            返回首页
                        </Button>
                    </Link>
                </div>

                {/* Additional Help */}
                <Card className="bg-muted/50">
                    <CardContent className="pt-6">
                        <div className="text-center space-y-2">
                            <p className="text-sm text-muted-foreground">
                                需要帮助?
                            </p>
                            <Button variant="link" className="h-auto p-0" disabled>
                                联系支持团队
                            </Button>
                        </div>
                    </CardContent>
                </Card>
            </div>
        </div>
    )
}

/pages/User.tsx

复制代码
// User.tsx

import { Link, useLoaderData } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Search, Mail, Phone, MapPin } from 'lucide-react'
import { useState } from 'react'
import type { RouteLoaderData } from '@/router'

interface User {
    id: number
    name: string
    email: string
    phone: string
    website: string
    address: {
        city: string
        street: string
        suite: string
        zipcode: string
    }
    company: {
        name: string
    }
}

export default function Users() {
    const { users } = useLoaderData() as RouteLoaderData
    const [searchTerm, setSearchTerm] = useState('')
    const userList = users as User[]

    const filteredUsers = userList?.filter(user =>
        user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
        user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
        user.company.name.toLowerCase().includes(searchTerm.toLowerCase())
    )

    return (
        <div className="max-w-6xl space-y-6">
            {/* Header */}
            <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
                <div>
                    <h1 className="text-3xl font-bold tracking-tight">用户管理</h1>
                    <p className="text-muted-foreground">
                        管理应用程序的用户信息
                    </p>
                </div>
                <Badge variant="secondary" className="text-sm">
                    共 {filteredUsers?.length || 0} 位用户
                </Badge>
            </div>

            {/* Search and Filters */}
            <Card>
                <CardContent className="pt-6">
                    <div className="flex flex-col sm:flex-row gap-4">
                        <div className="relative flex-1">
                            <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
                            <Input
                                placeholder="搜索用户姓名、邮箱或公司..."
                                value={searchTerm}
                                onChange={(e) => setSearchTerm(e.target.value)}
                                className="pl-10"
                            />
                        </div>
                        <Button variant="outline">筛选</Button>
                    </div>
                </CardContent>
            </Card>

            {/* Users Grid */}
            {filteredUsers && filteredUsers.length > 0 ? (
                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                    {filteredUsers.map((user) => (
                        <Card key={user.id} className="hover:shadow-lg transition-shadow duration-200">
                            <CardHeader className="pb-4">
                                <div className="flex items-start justify-between">
                                    <div>
                                        <CardTitle className="text-lg">{user.name}</CardTitle>
                                        <CardDescription className="flex items-center gap-1 mt-1">
                                            <Mail className="h-3 w-3" />
                                            {user.email}
                                        </CardDescription>
                                    </div>
                                    <Badge variant="outline" className="text-xs">
                                        ID: {user.id}
                                    </Badge>
                                </div>
                            </CardHeader>
                            <CardContent className="space-y-3">
                                <div className="space-y-2 text-sm">
                                    <div className="flex items-center gap-2">
                                        <Phone className="h-3 w-3 text-muted-foreground" />
                                        <span>{user.phone}</span>
                                    </div>
                                    <div className="flex items-center gap-2">
                                        <MapPin className="h-3 w-3 text-muted-foreground" />
                                        <span>{user.address.city}</span>
                                    </div>
                                    <div className="pt-2">
                                        <span className="font-medium">公司:</span>
                                        <div className="text-muted-foreground">{user.company.name}</div>
                                    </div>
                                    <div>
                                        <span className="font-medium">网站:</span>
                                        <div className="text-muted-foreground">{user.website}</div>
                                    </div>
                                </div>

                                <div className="pt-4 flex justify-between items-center">
                                    <Badge variant="secondary" className="text-xs">
                                        {user.address.city}
                                    </Badge>
                                    <Link to={`/users/${user.id}`}>
                                        <Button size="sm">查看详情</Button>
                                    </Link>
                                </div>
                            </CardContent>
                        </Card>
                    ))}
                </div>
            ) : (
                <Card>
                    <CardContent className="pt-6">
                        <div className="text-center py-8">
                            <div className="text-muted-foreground">
                                {searchTerm ? '没有找到匹配的用户' : '暂无用户数据'}
                            </div>
                            {searchTerm && (
                                <Button
                                    variant="outline"
                                    onClick={() => setSearchTerm('')}
                                    className="mt-4"
                                >
                                    清除搜索
                                </Button>
                            )}
                        </div>
                    </CardContent>
                </Card>
            )}

            {/* API Info */}
            <Card className="bg-muted/50">
                <CardHeader>
                    <CardTitle className="text-sm">数据来源</CardTitle>
                    <CardDescription>
                        用户数据通过 React Router Framework Mode 的 loader 在路由级别加载
                    </CardDescription>
                </CardHeader>
                <CardContent>
                    <div className="text-xs font-mono bg-background p-3 rounded border">
                        {/*loader: async () => {'{'} ... {'}'}*/}
                    </div>
                </CardContent>
            </Card>
        </div>
    )
}

/pages/UserDetail.tsx

复制代码
// UserDetail.tsx


import { useLoaderData, useParams, Link, useNavigate } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { ArrowLeft, Mail, Phone, MapPin, Globe, Building, Navigation } from 'lucide-react'
import type { RouteLoaderData } from '@/router'

interface User {
    id: number
    name: string
    username: string
    email: string
    phone: string
    website: string
    address: {
        street: string
        suite: string
        city: string
        zipcode: string
        geo: {
            lat: string
            lng: string
        }
    }
    company: {
        name: string
        catchPhrase: string
        bs: string
    }
}

export default function UserDetail() {
    const { user } = useLoaderData() as RouteLoaderData
    const { userId } = useParams()
    const navigate = useNavigate()
    const userData = user as User

    if (!userData) {
        return (
            <div className="max-w-4xl mx-auto text-center space-y-6 py-12">
                <div className="space-y-2">
                    <h1 className="text-4xl font-bold">用户不存在</h1>
                    <p className="text-muted-foreground text-xl">
                        未找到 ID 为 {userId} 的用户
                    </p>
                </div>
                <div className="flex gap-4 justify-center">
                    <Button onClick={() => navigate(-1)} variant="outline">
                        <ArrowLeft className="w-4 h-4 mr-2" />
                        返回上一页
                    </Button>
                    <Link to="/users">
                        <Button>查看所有用户</Button>
                    </Link>
                </div>
            </div>
        )
    }

    return (
        <div className="max-w-4xl mx-auto space-y-6">
            {/* Header */}
            <div className="flex items-center gap-4">
                <Button
                    variant="outline"
                    size="icon"
                    onClick={() => navigate(-1)}
                    className="flex-shrink-0"
                >
                    <ArrowLeft className="h-4 w-4" />
                </Button>
                <div className="flex-1">
                    <h1 className="text-3xl font-bold tracking-tight">{userData.name}</h1>
                    <p className="text-muted-foreground">用户详细信息</p>
                </div>
                <Badge variant="secondary">ID: {userData.id}</Badge>
            </div>

            <div className="grid md:grid-cols-3 gap-6">
                {/* Main Info */}
                <div className="md:col-span-2 space-y-6">
                    {/* Basic Information */}
                    <Card>
                        <CardHeader>
                            <CardTitle>基本信息</CardTitle>
                            <CardDescription>用户的基础资料</CardDescription>
                        </CardHeader>
                        <CardContent className="space-y-4">
                            <div className="grid sm:grid-cols-2 gap-4">
                                <div>
                                    <label className="text-sm font-medium text-muted-foreground">用户名</label>
                                    <p className="text-lg font-semibold">{userData.username}</p>
                                </div>
                                <div>
                                    <label className="text-sm font-medium text-muted-foreground">用户ID</label>
                                    <p className="text-lg font-semibold">{userData.id}</p>
                                </div>
                            </div>

                            <div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
                                <Mail className="h-4 w-4 text-muted-foreground" />
                                <span>{userData.email}</span>
                            </div>

                            <div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
                                <Phone className="h-4 w-4 text-muted-foreground" />
                                <span>{userData.phone}</span>
                            </div>

                            <div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
                                <Globe className="h-4 w-4 text-muted-foreground" />
                                <span>{userData.website}</span>
                            </div>
                        </CardContent>
                    </Card>

                    {/* Company Information */}
                    <Card>
                        <CardHeader>
                            <CardTitle className="flex items-center gap-2">
                                <Building className="h-5 w-5" />
                                公司信息
                            </CardTitle>
                        </CardHeader>
                        <CardContent className="space-y-4">
                            <div>
                                <label className="text-sm font-medium text-muted-foreground">公司名称</label>
                                <p className="text-lg font-semibold">{userData.company.name}</p>
                            </div>

                            <div>
                                <label className="text-sm font-medium text-muted-foreground">公司标语</label>
                                <p className="text-lg italic">"{userData.company.catchPhrase}"</p>
                            </div>

                            <div>
                                <label className="text-sm font-medium text-muted-foreground">业务描述</label>
                                <p className="text-lg">{userData.company.bs}</p>
                            </div>
                        </CardContent>
                    </Card>
                </div>

                {/* Sidebar */}
                <div className="space-y-6">
                    {/* Address Information */}
                    <Card>
                        <CardHeader>
                            <CardTitle className="flex items-center gap-2">
                                <MapPin className="h-5 w-5" />
                                地址信息
                            </CardTitle>
                        </CardHeader>
                        <CardContent className="space-y-3">
                            <div>
                                <label className="text-sm font-medium text-muted-foreground">街道</label>
                                <p>{userData.address.street}</p>
                            </div>

                            <div>
                                <label className="text-sm font-medium text-muted-foreground">套房/公寓</label>
                                <p>{userData.address.suite}</p>
                            </div>

                            <div>
                                <label className="text-sm font-medium text-muted-foreground">城市</label>
                                <p>{userData.address.city}</p>
                            </div>

                            <div>
                                <label className="text-sm font-medium text-muted-foreground">邮编</label>
                                <p>{userData.address.zipcode}</p>
                            </div>

                            <div className="pt-2">
                                <div className="flex items-center gap-2 text-sm text-muted-foreground">
                                    <Navigation className="h-4 w-4" />
                                    坐标: {userData.address.geo.lat}, {userData.address.geo.lng}
                                </div>
                            </div>
                        </CardContent>
                    </Card>

                    {/* Quick Actions */}
                    <Card>
                        <CardHeader>
                            <CardTitle>操作</CardTitle>
                        </CardHeader>
                        <CardContent className="space-y-3">
                            <Link to="/users" className="block">
                                <Button variant="outline" className="w-full justify-start">
                                    <ArrowLeft className="h-4 w-4 mr-2" />
                                    返回用户列表
                                </Button>
                            </Link>
                            <Button variant="outline" className="w-full justify-start" disabled>
                                编辑用户
                            </Button>
                            <Button variant="outline" className="w-full justify-start" disabled>
                                发送消息
                            </Button>
                        </CardContent>
                    </Card>
                </div>
            </div>

            {/* API Info */}
            <Card className="bg-muted/50">
                <CardHeader>
                    <CardTitle className="text-sm">数据加载</CardTitle>
                    <CardDescription>
                        此页面数据通过 React Router Framework Mode 的动态路由 loader 加载
                    </CardDescription>
                </CardHeader>
                <CardContent>
                    <div className="text-xs font-mono bg-background p-3 rounded border">
                        {`loader: async ({ params }) => {
  const user = await userService.getUserById(Number(params.userId))
  return { user }
}`}
                    </div>
                </CardContent>
            </Card>
        </div>
    )
}

/components/ErrorBoundary.tsx

复制代码
// ErrorBoundary.tsx


import { useRouteError, isRouteErrorResponse, Link } from 'react-router-dom'
import { Button } from '@/components/ui/button'

export default function ErrorBoundary() {
    const error = useRouteError()

    let errorMessage: string
    let errorStatus: number | undefined

    if (isRouteErrorResponse(error)) {
        errorStatus = error.status
        errorMessage = error.statusText || error.data?.message || 'An error occurred'
    } else if (error instanceof Error) {
        errorMessage = error.message
    } else {
        errorMessage = 'Unknown error'
    }

    return (
        <div className="min-h-screen bg-background flex items-center justify-center">
            <div className="text-center">
                <h1 className="text-4xl font-bold mb-4">
                    {errorStatus ? `Error ${errorStatus}` : 'Error'}
                </h1>
                <p className="text-xl text-muted-foreground mb-8">{errorMessage}</p>
                <Link to="/">
                    <Button>Go Back Home</Button>
                </Link>
            </div>
        </div>
    )
}

/components/app-sidebar.tsx

复制代码
// app-sidebar.tsx


import { Calendar, Home, Inbox, ChevronDown } from "lucide-react"

import {
    Sidebar,
    SidebarContent,
    SidebarGroup,
    SidebarGroupContent,
    SidebarGroupLabel,
    SidebarMenu,
    SidebarMenuButton,
    SidebarMenuItem,
    SidebarMenuSub,
    SidebarMenuSubItem
} from "@/components/ui/sidebar"
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible'

// Menu items.
const items = [
    {
        title: "Home",
        url: "/",
        icon: Home,
    },
    {
        title: "about",
        url: "/about",
        icon: Inbox,
    },
    {
        title: "users",
        url: "/users",
        icon: Calendar,
    }
]

export function AppSidebar() {
    return (
        <Sidebar className="sidebar-custom-div">
            <SidebarContent>
                <SidebarGroup>
                    <SidebarGroupContent>
                        <SidebarMenu>
                            {items.map((item) => (
                                <SidebarMenuItem key={item.title}>
                                    <SidebarMenuButton asChild>
                                        <a href={item.url}>
                                            <item.icon />
                                            <span>{item.title}</span>
                                        </a>
                                    </SidebarMenuButton>
                                </SidebarMenuItem>
                            ))}
                            <Collapsible defaultOpen className="group/collapsible">
                                <SidebarMenuItem>
                                    <CollapsibleTrigger asChild className="no-focus-border">
                                        <SidebarMenuButton><Home/>111
                                    <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" /></SidebarMenuButton>
                                </CollapsibleTrigger>
                                    <CollapsibleContent>
                                        <SidebarMenuSub>
                                            <SidebarMenuSubItem>
                                                <SidebarMenuButton asChild>
                                                    <a href={'/about'}>
                                                        <span>about</span>
                                                    </a>
                                                </SidebarMenuButton>
                                            </SidebarMenuSubItem>
                                            <SidebarMenuSubItem>
                                                <SidebarMenuButton asChild>
                                                    <a href={'/'}>
                                                        <span>home</span>
                                                    </a>
                                                </SidebarMenuButton>
                                            </SidebarMenuSubItem>
                                        </SidebarMenuSub>
                                    </CollapsibleContent>
                                </SidebarMenuItem>
                            </Collapsible>
                        </SidebarMenu>
                    </SidebarGroupContent>
                </SidebarGroup>
            </SidebarContent>
        </Sidebar>
    )
}

配置接口

/api/client.ts

复制代码
import axios from 'axios'

export const apiClient = axios.create({
    baseURL: 'https://jsonplaceholder.typicode.com',
    timeout: 10000,
    headers: {
        'Content-Type': 'application/json',
    },
})

apiClient.interceptors.request.use(
    (config) => {
        const token = localStorage.getItem('token')
        if (token) {
            config.headers.Authorization = `Bearer ${token}`
        }
        return config
    },
    (error) => Promise.reject(error)
)

apiClient.interceptors.response.use(
    (response) => response,
    (error) => {
        console.error('API Error:', error.response?.data || error.message)
        return Promise.reject(error)
    }
)

/api/userService.ts

复制代码
// userService.ts


import { apiClient } from './client'

export interface User {
    id: number
    name: string
    email: string
    phone: string
}

export const userService = {
    getUsers: (): Promise<User[]> =>
        apiClient.get('/users').then(response => response.data),

    getUserById: (id: number): Promise<User> =>
        apiClient.get(`/users/${id}`).then(response => response.data),

    createUser: (user: Omit<User, 'id'>): Promise<User> =>
        apiClient.post('/users', user).then(response => response.data),
}

/lib/utils.ts

复制代码
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

/hooks/use-mobile.tsx

复制代码
import * as React from "react"

const MOBILE_BREAKPOINT = 768

export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return !!isMobile
}

/stores/counterStore.ts

复制代码
import { create } from 'zustand'

interface CounterState {
    count: number
    increase: () => void
    decrease: () => void
    reset: () => void
}

export const useCounterStore = create<CounterState>((set) => ({
    count: 0,
    increase: () => set((state) => ({ count: state.count + 1 })),
    decrease: () => set((state) => ({ count: state.count - 1 })),
    reset: () => set({ count: 0 }),
}))
相关推荐
fangzhanpeng1685 天前
快速掌握C#语言基础知识点(1.基础数据类型)
开发语言·c#·1024程序员节
无己心7 天前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u01030552713 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u01030552713 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝14 天前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u01030552715 天前
Java图像处理实战指南
人工智能·1024程序员节
u01030552715 天前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u01030552717 天前
Java实用类应用精讲
人工智能·1024程序员节
u01030552718 天前
Java I/O核心操作实战
人工智能·1024程序员节
u01030552719 天前
ArrayList操作详解与实战应用
人工智能·1024程序员节