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 }),
}))
相关推荐
日日行不惧千万里1 小时前
MediaMTX详解
1024程序员节
金融小师妹5 小时前
基于LSTM-GARCH模型:三轮黄金周期特征提取与多因子定价机制解构
人工智能·深度学习·1024程序员节
自信1504130575912 小时前
初学者小白复盘23之——联合与枚举
c语言·1024程序员节
CoderYanger2 天前
B.双指针——3194. 最小元素和最大元素的最小平均值
java·开发语言·数据结构·算法·leetcode·职场和发展·1024程序员节
自信150413057592 天前
初学者小白复盘22之——结构体
c语言·数据结构·1024程序员节
_Power_Y3 天前
黑马点评逻辑梳理+面试题
数据库·redis·学习·1024程序员节
开开心心_Every3 天前
Excel图片提取工具,批量导出无限制
学习·pdf·华为云·.net·excel·harmonyos·1024程序员节
爱喝水的鱼丶4 天前
SAP-ABAP:SAP概述:数据处理的系统、应用与产品
运维·学习·sap·abap·1024程序员节
CoderJia程序员甲5 天前
GitHub 热榜项目 - 日榜(2025-11-13)
ai·开源·github·1024程序员节·ai教程