0%

使用AI配合shadcn+tailwindcss快速生成界面

AI辅助的UI开发正在改变传统的设计实现流程,通过shadcn和TailwindCSS的强大组件生态,结合AI的智能生成能力,可以极大提升界面开发效率。

介绍

  在现代前端开发中,快速实现高质量的用户界面已成为开发效率的关键因素。shadcn/ui组件库配合TailwindCSS提供了一套优雅的UI组件解决方案,而AI工具的引入则为这一流程带来了革命性的变化。通过合理利用AI的智能生成能力,开发者可以更快地实现设计想法,保持设计系统的一致性,并减少重复性的编码工作。

shadcn与TailwindCSS基础

shadcn组件库核心概念

shadcn/ui是一套基于Radix UI和TailwindCSS的可访问、可定制的组件库。

1
2
3
4
5
# 初始化shadcn项目
npx shadcn-ui@latest init

# 添加组件
npx shadcn-ui@latest add button card input label textarea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"

export { Button, buttonVariants }

TailwindCSS配置优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

AI辅助界面生成策略

GitHub Copilot辅助开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 使用AI生成复杂表单组件
// Prompt: "Create a form component with validation, loading states, and success feedback"
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';

interface FormState {
name: string;
email: string;
message: string;
}

interface FormErrors {
name?: string;
email?: string;
message?: string;
}

const ContactForm = () => {
const [formData, setFormData] = useState<FormState>({ name: '', email: '', message: '' });
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);

const validate = (): boolean => {
const newErrors: FormErrors = {};

if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}

if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}

if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!validate()) return;

setIsSubmitting(true);

try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));

setIsSubmitted(true);
setFormData({ name: '', email: '', message: '' });
} catch (error) {
console.error('Form submission error:', error);
} finally {
setIsSubmitting(false);
}
};

if (isSubmitted) {
return (
<Card className="w-full max-w-md mx-auto mt-8">
<CardHeader>
<CardTitle>Thank You!</CardTitle>
<CardDescription>Your message has been sent successfully.</CardDescription>
</CardHeader>
<CardFooter>
<Button
onClick={() => setIsSubmitted(false)}
className="w-full"
>
Send Another Message
</Button>
</CardFooter>
</Card>
);
}

return (
<Card className="w-full max-w-md mx-auto mt-8">
<CardHeader>
<CardTitle>Contact Us</CardTitle>
<CardDescription>Fill out the form to get in touch.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className={errors.name ? 'border-red-500' : ''}
placeholder="Enter your name"
/>
{errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
</div>

<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
className={errors.email ? 'border-red-500' : ''}
placeholder="Enter your email"
/>
{errors.email && <p className="text-sm text-red-500">{errors.email}</p>}
</div>

<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<textarea
id="message"
value={formData.message}
onChange={(e) => setFormData({...formData, message: e.target.value})}
className={`w-full resize-none rounded-md border px-3 py-2 ${errors.message ? 'border-red-500' : 'border-input'}`}
placeholder="Enter your message"
rows={4}
/>
{errors.message && <p className="text-sm text-red-500">{errors.message}</p>}
</div>
</form>
</CardContent>
<CardFooter>
<Button
type="submit"
onClick={handleSubmit}
disabled={isSubmitting}
className="w-full"
>
{isSubmitting ? (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</span>
) : 'Send Message'}
</Button>
</CardFooter>
</Card>
);
};

export default ContactForm;

AI生成复杂布局组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// 使用AI生成仪表板布局
// Prompt: "Create a responsive dashboard layout with sidebar, header, and content area using shadcn components"
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { Skeleton } from '@/components/ui/skeleton';
import { Menu, Package, Users, LineChart, Settings, CreditCard, Truck } from 'lucide-react';

const DashboardLayout = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeTab, setActiveTab] = useState('dashboard');

const sidebarItems = [
{ id: 'dashboard', label: 'Dashboard', icon: LineChart },
{ id: 'orders', label: 'Orders', icon: Package },
{ id: 'products', label: 'Products', icon: CreditCard },
{ id: 'customers', label: 'Customers', icon: Users },
{ id: 'analytics', label: 'Analytics', icon: LineChart },
{ id: 'delivery', label: 'Delivery', icon: Truck },
{ id: 'settings', label: 'Settings', icon: Settings },
];

const renderContent = () => {
switch(activeTab) {
case 'dashboard':
return <DashboardContent />;
case 'orders':
return <OrdersContent />;
case 'products':
return <ProductsContent />;
default:
return <DashboardContent />;
}
};

return (
<div className="flex h-screen bg-muted/40">
{/* Mobile sidebar */}
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="absolute left-4 top-4 z-50 flex h-8 w-8 items-center justify-center rounded-lg bg-background md:hidden"
>
<Menu className="h-4 w-4" />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="inset-y-0 z-50 w-72 bg-background p-0">
<nav className="grid gap-2 py-4 text-lg font-medium">
{sidebarItems.map((item) => {
const Icon = item.icon;
return (
<button
key={item.id}
onClick={() => {
setActiveTab(item.id);
setSidebarOpen(false);
}}
className={`mx-[-0.65rem] flex items-center gap-4 rounded-xl px-4 py-3 ${
activeTab === item.id
? 'bg-muted text-primary'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Icon className="h-5 w-5" />
{item.label}
</button>
);
})}
</nav>
</SheetContent>
</Sheet>

{/* Desktop sidebar */}
<aside className="hidden md:flex md:w-72 md:flex-col">
<div className="flex h-16 items-center border-b px-6">
<h2 className="text-lg font-semibold">Acme Inc</h2>
</div>
<nav className="grid flex-1 items-start gap-2 p-4 text-lg font-medium">
{sidebarItems.map((item) => {
const Icon = item.icon;
return (
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={`mx-[-0.65rem] flex items-center gap-4 rounded-xl px-4 py-3 ${
activeTab === item.id
? 'bg-muted text-primary'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Icon className="h-5 w-5" />
{item.label}
</button>
);
})}
</nav>
</aside>

<div className="flex flex-col flex-1 overflow-hidden">
<header className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-4 border-b bg-background px-6">
<div className="ml-auto flex items-center gap-4">
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg">
<Settings className="h-4 w-4" />
</Button>
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<span className="text-sm font-medium">JD</span>
</div>
</div>
</header>

<main className="flex-1 overflow-y-auto p-6">
{renderContent()}
</main>
</div>
</div>
);
};

// 仪表板内容组件
const DashboardContent = () => {
return (
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Subscriptions</CardTitle>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">+180.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sales</CardTitle>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12,234</div>
<p className="text-xs text-muted-foreground">+19% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Now</CardTitle>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<p className="text-xs text-muted-foreground">+201 since last hour</p>
</CardContent>
</Card>
</div>

<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<div className="h-[200px] flex items-center justify-center">
<Skeleton className="h-40 w-full" />
</div>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Recent Sales</CardTitle>
<CardDescription>You made 265 sales this month.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-8">
{[1, 2, 3, 4].map((item) => (
<div key={item} className="flex items-center">
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">Liam Johnson</p>
<p className="text-sm text-muted-foreground">liam@example.com</p>
</div>
<div className="ml-auto font-medium">+$265.00</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
};

const OrdersContent = () => {
return (
<Card>
<CardHeader>
<CardTitle>Orders</CardTitle>
<CardDescription>Manage your orders here.</CardDescription>
</CardHeader>
<CardContent>
<div className="h-96 flex items-center justify-center">
<p className="text-muted-foreground">Orders content will be displayed here</p>
</div>
</CardContent>
</Card>
);
};

const ProductsContent = () => {
return (
<Card>
<CardHeader>
<CardTitle>Products</CardTitle>
<CardDescription>Manage your products here.</CardDescription>
</CardHeader>
<CardContent>
<div className="h-96 flex items-center justify-center">
<p className="text-muted-foreground">Products content will be displayed here</p>
</div>
</CardContent>
</Card>
);
};

export default DashboardLayout;

AI提示词优化策略

高效提示词模式

1
2
3
# AI UI生成提示词模板

## 基础组件生成

Create a [component type] component with [styling framework] that [functionality description].
The component should be accessible and follow best practices.

1
2

## 复杂布局生成

Create a responsive [layout type] layout using [frameworks] with the following requirements:

  • [Requirement 1]
  • [Requirement 2]
  • [Requirement 3]

The design should follow modern UI/UX principles and be mobile-responsive.

1
2

## 表单验证生成

Create a form component with [number] fields that includes:

  • Client-side validation
  • Loading states
  • Success/error feedback
  • Accessibility features

Use [UI library] components and [CSS framework] for styling.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
```

### 实用AI提示词示例

```typescript
// 提示词:生成带有数据表格的管理界面
/*
Create a data management interface with the following components:
1. Searchable and filterable data table
2. Row selection capabilities
3. Bulk actions toolbar
4. Pagination controls
5. Loading and empty states

Use shadcn/ui components including:
- Table with advanced features
- Dialog for editing records
- Dropdown for bulk actions
- Combobox for filtering

The design should be fully responsive and follow accessibility standards.
Include sample data and mock API integration.
*/

// 生成的数据表格组件
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';

interface User {
id: string;
name: string;
email: string;
role: string;
status: 'active' | 'inactive' | 'pending';
joinedDate: string;
}

const UserManager = () => {
const [users, setUsers] = useState<User[]>([]);
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

// 模拟数据加载
useEffect(() => {
const mockUsers: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'active', joinedDate: '2023-01-15' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'User', status: 'active', joinedDate: '2023-02-20' },
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', role: 'Editor', status: 'inactive', joinedDate: '2023-03-10' },
{ id: '4', name: 'Alice Williams', email: 'alice@example.com', role: 'User', status: 'pending', joinedDate: '2023-04-05' },
{ id: '5', name: 'Charlie Brown', email: 'charlie@example.com', role: 'Admin', status: 'active', joinedDate: '2023-05-12' },
];

setTimeout(() => {
setUsers(mockUsers);
setFilteredUsers(mockUsers);
setLoading(false);
}, 500);
}, []);

// 搜索过滤
useEffect(() => {
if (!searchTerm) {
setFilteredUsers(users);
} else {
const filtered = users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.role.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filtered);
}
}, [searchTerm, users]);

const toggleRowSelection = (id: string) => {
const newSelected = new Set(selectedRows);
if (newSelected.has(id)) {
newSelected.delete(id);
} else {
newSelected.add(id);
}
setSelectedRows(newSelected);
};

const toggleSelectAll = () => {
if (selectedRows.size === filteredUsers.length) {
setSelectedRows(new Set());
} else {
const allIds = new Set(filteredUsers.map(user => user.id));
setSelectedRows(allIds);
}
};

const handleBulkDelete = () => {
if (window.confirm(`Are you sure you want to delete ${selectedRows.size} users?`)) {
setUsers(users.filter(user => !selectedRows.has(user.id)));
setSelectedRows(new Set());
}
};

const handleEditUser = (user: User) => {
setEditingUser(user);
setIsDialogOpen(true);
};

const handleDialogSave = (updatedUser: User) => {
setUsers(users.map(user => user.id === updatedUser.id ? updatedUser : user));
setIsDialogOpen(false);
setEditingUser(null);
};

const getStatusBadgeVariant = (status: User['status']) => {
switch(status) {
case 'active': return 'bg-green-500';
case 'inactive': return 'bg-gray-500';
case 'pending': return 'bg-yellow-500';
default: return 'bg-gray-500';
}
};

if (loading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-4">
<div className="h-10 bg-muted rounded w-1/4"></div>
<div className="h-96 bg-muted rounded"></div>
</div>
</div>
);
}

return (
<div className="p-6 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 className="text-2xl font-bold">User Management</h1>
<div className="flex gap-2">
<Button
onClick={() => console.log('Add new user')}
disabled={selectedRows.size > 0}
>
Add User
</Button>
{selectedRows.size > 0 && (
<Button
variant="destructive"
onClick={handleBulkDelete}
>
Delete Selected ({selectedRows.size})
</Button>
)}
</div>
</div>

<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Input
placeholder="Search users..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
<svg
className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>

<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedRows.size === filteredUsers.length && filteredUsers.length > 0}
onCheckedChange={toggleSelectAll}
/>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Joined</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
No users found
</TableCell>
</TableRow>
) : (
filteredUsers.map((user) => (
<TableRow key={user.id}>
<TableCell>
<Checkbox
checked={selectedRows.has(user.id)}
onCheckedChange={() => toggleRowSelection(user.id)}
/>
</TableCell>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell>
<Badge className={getStatusBadgeVariant(user.status)}>
{user.status}
</Badge>
</TableCell>
<TableCell>{user.joinedDate}</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditUser(user)}
>
Edit
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>

<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Showing {filteredUsers.length} of {users.length} users
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm">
Previous
</Button>
<Button size="sm">
Next
</Button>
</div>
</div>

{/* 编辑对话框 */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit User</DialogTitle>
</DialogHeader>
{editingUser && (
<EditUserForm
user={editingUser}
onSave={handleDialogSave}
onCancel={() => setIsDialogOpen(false)}
/>
)}
</DialogContent>
</Dialog>
</div>
);
};

// 编辑用户表单组件
const EditUserForm = ({ user, onSave, onCancel }: {
user: User;
onSave: (updatedUser: User) => void;
onCancel: () => void
}) => {
const [formData, setFormData] = useState<User>(user);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(formData);
};

return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Name</label>
<Input
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<Input
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Role</label>
<select
value={formData.role}
onChange={(e) => setFormData({...formData, role: e.target.value})}
className="w-full rounded-md border border-input bg-background px-3 py-2"
>
<option value="Admin">Admin</option>
<option value="Editor">Editor</option>
<option value="User">User</option>
</select>
</div>
<div className="flex justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Save Changes</Button>
</div>
</form>
);
};

export default UserManager;

性能优化与最佳实践

组件优化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 优化的表格组件 - 使用虚拟滚动
import { useState, useMemo, useCallback } from 'react';
import { FixedSizeList as List } from 'react-window';

const VirtualizedTable = ({ data, columns }) => {
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null);
const [filterText, setFilterText] = useState('');

// 数据过滤和排序
const processedData = useMemo(() => {
let filtered = data;

// 过滤
if (filterText) {
filtered = data.filter(item =>
Object.values(item).some(val =>
String(val).toLowerCase().includes(filterText.toLowerCase())
)
);
}

// 排序
if (sortConfig !== null) {
filtered = [...filtered].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}

return filtered;
}, [data, filterText, sortConfig]);

const handleSort = useCallback((key: string) => {
let direction: 'asc' | 'desc' = 'asc';
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
}, [sortConfig]);

const Row = ({ index, style }) => {
const item = processedData[index];
return (
<div style={style} className="flex border-b hover:bg-muted/50">
{columns.map((column, colIndex) => (
<div key={colIndex} className="flex-1 p-2 truncate">
{item[column.key]}
</div>
))}
</div>
);
};

return (
<div className="space-y-4">
<div className="flex gap-2">
<input
type="text"
placeholder="Filter..."
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
className="flex-1 rounded-md border border-input px-3 py-2"
/>
</div>

<div className="rounded-md border">
<div className="flex border-b font-medium">
{columns.map((column, index) => (
<div
key={index}
className="flex-1 p-2 cursor-pointer hover:bg-muted"
onClick={() => handleSort(column.key)}
>
{column.title}
{sortConfig?.key === column.key && (
<span>{sortConfig.direction === 'asc' ? ' ↑' : ' ↓'}</span>
)}
</div>
))}
</div>
<List
height={400}
itemCount={processedData.length}
itemSize={40}
width="100%"
>
{Row}
</List>
</div>
</div>
);
};

AI辅助的界面开发能够显著提升开发效率,但需要结合良好的组件设计和性能优化策略,确保生成的代码既高效又可维护。

总结

  AI配合shadcn和TailwindCSS的界面生成方式代表了现代前端开发的趋势。通过合理利用AI工具,我们可以:

  1. 加速开发:快速生成常见UI组件和布局
  2. 保持一致性:使用标准化的组件库确保设计系统一致性
  3. 提高质量:AI生成的代码通常遵循最佳实践
  4. 减少重复:自动化处理常见的界面开发任务

  然而,我们也需要注意AI生成代码的审查和优化,确保生成的组件符合项目需求并具备良好的性能特征。通过合理运用AI工具和现代UI框架,我们可以构建出高效、美观且可维护的用户界面。

bulb