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/ # API 服务层</div>
<div className="ml-4">├── components/ # 可复用组件</div>
<div className="ml-8">├── ui/ # Shadcn UI 组件</div>
<div className="ml-4">├── pages/ # 页面组件</div>
<div className="ml-4">├── stores/ # 状态管理</div>
<div className="ml-4">├── hooks/ # 自定义 Hooks</div>
<div className="ml-4">├── lib/ # 工具函数</div>
<div className="ml-4">└── router/ # 路由配置</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 }),
}))