mcp-probe-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +607 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +553 -0
- package/build/tools/check_deps.d.ts +13 -0
- package/build/tools/check_deps.js +204 -0
- package/build/tools/code_review.d.ts +13 -0
- package/build/tools/code_review.js +138 -0
- package/build/tools/convert.d.ts +13 -0
- package/build/tools/convert.js +575 -0
- package/build/tools/debug.d.ts +13 -0
- package/build/tools/debug.js +78 -0
- package/build/tools/detect_shell.d.ts +6 -0
- package/build/tools/detect_shell.js +138 -0
- package/build/tools/explain.d.ts +13 -0
- package/build/tools/explain.js +369 -0
- package/build/tools/fix.d.ts +13 -0
- package/build/tools/fix.js +290 -0
- package/build/tools/genapi.d.ts +13 -0
- package/build/tools/genapi.js +152 -0
- package/build/tools/genchangelog.d.ts +13 -0
- package/build/tools/genchangelog.js +227 -0
- package/build/tools/gencommit.d.ts +13 -0
- package/build/tools/gencommit.js +95 -0
- package/build/tools/gendoc.d.ts +13 -0
- package/build/tools/gendoc.js +208 -0
- package/build/tools/genpr.d.ts +13 -0
- package/build/tools/genpr.js +173 -0
- package/build/tools/genreadme.d.ts +13 -0
- package/build/tools/genreadme.js +613 -0
- package/build/tools/gensql.d.ts +13 -0
- package/build/tools/gensql.js +307 -0
- package/build/tools/gentest.d.ts +13 -0
- package/build/tools/gentest.js +155 -0
- package/build/tools/genui.d.ts +13 -0
- package/build/tools/genui.js +781 -0
- package/build/tools/index.d.ts +22 -0
- package/build/tools/index.js +22 -0
- package/build/tools/init_project.d.ts +13 -0
- package/build/tools/init_project.js +142 -0
- package/build/tools/init_setting.d.ts +13 -0
- package/build/tools/init_setting.js +47 -0
- package/build/tools/perf.d.ts +13 -0
- package/build/tools/perf.js +359 -0
- package/build/tools/refactor.d.ts +13 -0
- package/build/tools/refactor.js +318 -0
- package/build/tools/resolve_conflict.d.ts +13 -0
- package/build/tools/resolve_conflict.js +338 -0
- package/build/tools/split.d.ts +13 -0
- package/build/tools/split.js +577 -0
- package/package.json +66 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
// genui 工具实现
|
|
2
|
+
export async function genui(args) {
|
|
3
|
+
try {
|
|
4
|
+
const description = args?.description || "";
|
|
5
|
+
const framework = args?.framework || "react"; // react, vue, html
|
|
6
|
+
const message = `请生成以下 UI 组件:
|
|
7
|
+
|
|
8
|
+
📝 **组件描述**:
|
|
9
|
+
${description || "请描述需要的 UI 组件"}
|
|
10
|
+
|
|
11
|
+
⚛️ **框架**:${framework}
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## UI 组件生成指南
|
|
16
|
+
|
|
17
|
+
### 第一步:理解需求
|
|
18
|
+
|
|
19
|
+
**组件类型**:
|
|
20
|
+
- 基础组件(Button, Input, Card)
|
|
21
|
+
- 表单组件(Form, Select, Checkbox)
|
|
22
|
+
- 数据展示(Table, List, Grid)
|
|
23
|
+
- 反馈组件(Modal, Toast, Loading)
|
|
24
|
+
- 导航组件(Menu, Tabs, Breadcrumb)
|
|
25
|
+
- 布局组件(Layout, Container, Flex)
|
|
26
|
+
|
|
27
|
+
### 第二步:设计原则
|
|
28
|
+
|
|
29
|
+
**1️⃣ 组件化**
|
|
30
|
+
- 单一职责
|
|
31
|
+
- 可复用
|
|
32
|
+
- 可组合
|
|
33
|
+
|
|
34
|
+
**2️⃣ 可访问性(A11y)**
|
|
35
|
+
- 语义化 HTML
|
|
36
|
+
- ARIA 属性
|
|
37
|
+
- 键盘导航
|
|
38
|
+
- 屏幕阅读器支持
|
|
39
|
+
|
|
40
|
+
**3️⃣ 响应式**
|
|
41
|
+
- 移动端优先
|
|
42
|
+
- 断点设计
|
|
43
|
+
- 弹性布局
|
|
44
|
+
|
|
45
|
+
**4️⃣ 性能**
|
|
46
|
+
- 懒加载
|
|
47
|
+
- 虚拟滚动
|
|
48
|
+
- 防抖节流
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## React 组件示例
|
|
53
|
+
|
|
54
|
+
### 基础 Button 组件
|
|
55
|
+
\`\`\`tsx
|
|
56
|
+
import React from 'react';
|
|
57
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
58
|
+
|
|
59
|
+
const buttonVariants = cva(
|
|
60
|
+
// 基础样式
|
|
61
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
62
|
+
{
|
|
63
|
+
variants: {
|
|
64
|
+
variant: {
|
|
65
|
+
default: "bg-blue-600 text-white hover:bg-blue-700",
|
|
66
|
+
destructive: "bg-red-600 text-white hover:bg-red-700",
|
|
67
|
+
outline: "border border-gray-300 bg-transparent hover:bg-gray-100",
|
|
68
|
+
ghost: "hover:bg-gray-100",
|
|
69
|
+
link: "text-blue-600 underline-offset-4 hover:underline",
|
|
70
|
+
},
|
|
71
|
+
size: {
|
|
72
|
+
default: "h-10 px-4 py-2",
|
|
73
|
+
sm: "h-9 px-3",
|
|
74
|
+
lg: "h-11 px-8",
|
|
75
|
+
icon: "h-10 w-10",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultVariants: {
|
|
79
|
+
variant: "default",
|
|
80
|
+
size: "default",
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export interface ButtonProps
|
|
86
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
87
|
+
VariantProps<typeof buttonVariants> {
|
|
88
|
+
isLoading?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
92
|
+
({ className, variant, size, isLoading, children, ...props }, ref) => {
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
className={buttonVariants({ variant, size, className })}
|
|
96
|
+
ref={ref}
|
|
97
|
+
disabled={isLoading || props.disabled}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
{isLoading && (
|
|
101
|
+
<svg
|
|
102
|
+
className="mr-2 h-4 w-4 animate-spin"
|
|
103
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
104
|
+
fill="none"
|
|
105
|
+
viewBox="0 0 24 24"
|
|
106
|
+
>
|
|
107
|
+
<circle
|
|
108
|
+
className="opacity-25"
|
|
109
|
+
cx="12"
|
|
110
|
+
cy="12"
|
|
111
|
+
r="10"
|
|
112
|
+
stroke="currentColor"
|
|
113
|
+
strokeWidth="4"
|
|
114
|
+
/>
|
|
115
|
+
<path
|
|
116
|
+
className="opacity-75"
|
|
117
|
+
fill="currentColor"
|
|
118
|
+
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"
|
|
119
|
+
/>
|
|
120
|
+
</svg>
|
|
121
|
+
)}
|
|
122
|
+
{children}
|
|
123
|
+
</button>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
Button.displayName = "Button";
|
|
129
|
+
|
|
130
|
+
export { Button, buttonVariants };
|
|
131
|
+
\`\`\`
|
|
132
|
+
|
|
133
|
+
**使用示例:**
|
|
134
|
+
\`\`\`tsx
|
|
135
|
+
<Button variant="default" size="lg">
|
|
136
|
+
提交
|
|
137
|
+
</Button>
|
|
138
|
+
|
|
139
|
+
<Button variant="outline" isLoading>
|
|
140
|
+
加载中...
|
|
141
|
+
</Button>
|
|
142
|
+
|
|
143
|
+
<Button variant="ghost" size="icon">
|
|
144
|
+
<Icon />
|
|
145
|
+
</Button>
|
|
146
|
+
\`\`\`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### 表单输入组件
|
|
151
|
+
\`\`\`tsx
|
|
152
|
+
import React from 'react';
|
|
153
|
+
import { useController, Control } from 'react-hook-form';
|
|
154
|
+
|
|
155
|
+
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
156
|
+
label?: string;
|
|
157
|
+
error?: string;
|
|
158
|
+
helperText?: string;
|
|
159
|
+
control?: Control<any>;
|
|
160
|
+
name: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
164
|
+
({ label, error, helperText, className, control, name, ...props }, ref) => {
|
|
165
|
+
const { field } = useController({
|
|
166
|
+
name,
|
|
167
|
+
control,
|
|
168
|
+
defaultValue: props.defaultValue || '',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="w-full space-y-2">
|
|
173
|
+
{label && (
|
|
174
|
+
<label
|
|
175
|
+
htmlFor={name}
|
|
176
|
+
className="text-sm font-medium text-gray-700"
|
|
177
|
+
>
|
|
178
|
+
{label}
|
|
179
|
+
{props.required && <span className="text-red-500 ml-1">*</span>}
|
|
180
|
+
</label>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<input
|
|
184
|
+
{...field}
|
|
185
|
+
{...props}
|
|
186
|
+
ref={ref}
|
|
187
|
+
id={name}
|
|
188
|
+
aria-invalid={!!error}
|
|
189
|
+
aria-describedby={error ? \`\${name}-error\` : undefined}
|
|
190
|
+
className={\`
|
|
191
|
+
w-full px-3 py-2 border rounded-md
|
|
192
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500
|
|
193
|
+
disabled:bg-gray-100 disabled:cursor-not-allowed
|
|
194
|
+
\${error ? 'border-red-500' : 'border-gray-300'}
|
|
195
|
+
\${className || ''}
|
|
196
|
+
\`}
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
{error && (
|
|
200
|
+
<p id={\`\${name}-error\`} className="text-sm text-red-600">
|
|
201
|
+
{error}
|
|
202
|
+
</p>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{helperText && !error && (
|
|
206
|
+
<p className="text-sm text-gray-500">{helperText}</p>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
Input.displayName = 'Input';
|
|
214
|
+
\`\`\`
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Modal 弹窗组件
|
|
219
|
+
\`\`\`tsx
|
|
220
|
+
import React, { useEffect } from 'react';
|
|
221
|
+
import { createPortal } from 'react-dom';
|
|
222
|
+
|
|
223
|
+
interface ModalProps {
|
|
224
|
+
isOpen: boolean;
|
|
225
|
+
onClose: () => void;
|
|
226
|
+
title?: string;
|
|
227
|
+
children: React.ReactNode;
|
|
228
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export const Modal: React.FC<ModalProps> = ({
|
|
232
|
+
isOpen,
|
|
233
|
+
onClose,
|
|
234
|
+
title,
|
|
235
|
+
children,
|
|
236
|
+
size = 'md',
|
|
237
|
+
}) => {
|
|
238
|
+
// ESC 关闭
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const handleEsc = (e: KeyboardEvent) => {
|
|
241
|
+
if (e.key === 'Escape') onClose();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (isOpen) {
|
|
245
|
+
document.addEventListener('keydown', handleEsc);
|
|
246
|
+
document.body.style.overflow = 'hidden';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return () => {
|
|
250
|
+
document.removeEventListener('keydown', handleEsc);
|
|
251
|
+
document.body.style.overflow = 'unset';
|
|
252
|
+
};
|
|
253
|
+
}, [isOpen, onClose]);
|
|
254
|
+
|
|
255
|
+
if (!isOpen) return null;
|
|
256
|
+
|
|
257
|
+
const sizeClasses = {
|
|
258
|
+
sm: 'max-w-md',
|
|
259
|
+
md: 'max-w-lg',
|
|
260
|
+
lg: 'max-w-2xl',
|
|
261
|
+
xl: 'max-w-4xl',
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return createPortal(
|
|
265
|
+
<div
|
|
266
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
267
|
+
onClick={onClose}
|
|
268
|
+
>
|
|
269
|
+
{/* 背景遮罩 */}
|
|
270
|
+
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
|
271
|
+
|
|
272
|
+
{/* 弹窗内容 */}
|
|
273
|
+
<div
|
|
274
|
+
className={\`
|
|
275
|
+
relative bg-white rounded-lg shadow-xl
|
|
276
|
+
w-full mx-4 \${sizeClasses[size]}
|
|
277
|
+
max-h-[90vh] flex flex-col
|
|
278
|
+
animate-in fade-in slide-in-from-bottom-4
|
|
279
|
+
\`}
|
|
280
|
+
onClick={(e) => e.stopPropagation()}
|
|
281
|
+
role="dialog"
|
|
282
|
+
aria-modal="true"
|
|
283
|
+
aria-labelledby={title ? 'modal-title' : undefined}
|
|
284
|
+
>
|
|
285
|
+
{/* 头部 */}
|
|
286
|
+
{title && (
|
|
287
|
+
<div className="px-6 py-4 border-b">
|
|
288
|
+
<h2 id="modal-title" className="text-xl font-semibold">
|
|
289
|
+
{title}
|
|
290
|
+
</h2>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
{/* 关闭按钮 */}
|
|
295
|
+
<button
|
|
296
|
+
onClick={onClose}
|
|
297
|
+
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
|
298
|
+
aria-label="关闭"
|
|
299
|
+
>
|
|
300
|
+
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
301
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
302
|
+
</svg>
|
|
303
|
+
</button>
|
|
304
|
+
|
|
305
|
+
{/* 内容 */}
|
|
306
|
+
<div className="px-6 py-4 overflow-y-auto flex-1">
|
|
307
|
+
{children}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>,
|
|
311
|
+
document.body
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
\`\`\`
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### Table 数据表格
|
|
319
|
+
\`\`\`tsx
|
|
320
|
+
import React from 'react';
|
|
321
|
+
|
|
322
|
+
interface Column<T> {
|
|
323
|
+
key: keyof T | string;
|
|
324
|
+
title: string;
|
|
325
|
+
render?: (value: any, record: T, index: number) => React.ReactNode;
|
|
326
|
+
width?: string;
|
|
327
|
+
align?: 'left' | 'center' | 'right';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
interface TableProps<T> {
|
|
331
|
+
columns: Column<T>[];
|
|
332
|
+
data: T[];
|
|
333
|
+
rowKey: keyof T;
|
|
334
|
+
loading?: boolean;
|
|
335
|
+
onRowClick?: (record: T) => void;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function Table<T extends Record<string, any>>({
|
|
339
|
+
columns,
|
|
340
|
+
data,
|
|
341
|
+
rowKey,
|
|
342
|
+
loading,
|
|
343
|
+
onRowClick,
|
|
344
|
+
}: TableProps<T>) {
|
|
345
|
+
if (loading) {
|
|
346
|
+
return <div className="text-center py-8">加载中...</div>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (data.length === 0) {
|
|
350
|
+
return <div className="text-center py-8 text-gray-500">暂无数据</div>;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div className="overflow-x-auto">
|
|
355
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
356
|
+
<thead className="bg-gray-50">
|
|
357
|
+
<tr>
|
|
358
|
+
{columns.map((col, index) => (
|
|
359
|
+
<th
|
|
360
|
+
key={String(col.key) || index}
|
|
361
|
+
style={{ width: col.width }}
|
|
362
|
+
className={\`
|
|
363
|
+
px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider
|
|
364
|
+
text-\${col.align || 'left'}
|
|
365
|
+
\`}
|
|
366
|
+
>
|
|
367
|
+
{col.title}
|
|
368
|
+
</th>
|
|
369
|
+
))}
|
|
370
|
+
</tr>
|
|
371
|
+
</thead>
|
|
372
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
373
|
+
{data.map((record, rowIndex) => (
|
|
374
|
+
<tr
|
|
375
|
+
key={String(record[rowKey])}
|
|
376
|
+
onClick={() => onRowClick?.(record)}
|
|
377
|
+
className={onRowClick ? 'cursor-pointer hover:bg-gray-50' : ''}
|
|
378
|
+
>
|
|
379
|
+
{columns.map((col, colIndex) => {
|
|
380
|
+
const value = col.key in record ? record[col.key as keyof T] : undefined;
|
|
381
|
+
return (
|
|
382
|
+
<td
|
|
383
|
+
key={colIndex}
|
|
384
|
+
className={\`px-6 py-4 whitespace-nowrap text-sm text-\${col.align || 'left'}\`}
|
|
385
|
+
>
|
|
386
|
+
{col.render ? col.render(value, record, rowIndex) : String(value)}
|
|
387
|
+
</td>
|
|
388
|
+
);
|
|
389
|
+
})}
|
|
390
|
+
</tr>
|
|
391
|
+
))}
|
|
392
|
+
</tbody>
|
|
393
|
+
</table>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
\`\`\`
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Tailwind CSS 配置
|
|
402
|
+
|
|
403
|
+
\`\`\`js
|
|
404
|
+
// tailwind.config.js
|
|
405
|
+
module.exports = {
|
|
406
|
+
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
|
407
|
+
theme: {
|
|
408
|
+
extend: {
|
|
409
|
+
keyframes: {
|
|
410
|
+
'fade-in': {
|
|
411
|
+
from: { opacity: 0 },
|
|
412
|
+
to: { opacity: 1 },
|
|
413
|
+
},
|
|
414
|
+
'slide-in-from-bottom': {
|
|
415
|
+
from: { transform: 'translateY(1rem)' },
|
|
416
|
+
to: { transform: 'translateY(0)' },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
animation: {
|
|
420
|
+
'fade-in': 'fade-in 0.2s ease-out',
|
|
421
|
+
'slide-in-from-bottom-4': 'slide-in-from-bottom 0.3s ease-out',
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
plugins: [],
|
|
426
|
+
};
|
|
427
|
+
\`\`\`
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Vue 3 组件示例
|
|
432
|
+
|
|
433
|
+
### 基础 Button 组件 (Vue 3 + TypeScript)
|
|
434
|
+
\`\`\`vue
|
|
435
|
+
<template>
|
|
436
|
+
<button
|
|
437
|
+
:class="buttonClasses"
|
|
438
|
+
:disabled="isLoading || disabled"
|
|
439
|
+
v-bind="$attrs"
|
|
440
|
+
>
|
|
441
|
+
<svg
|
|
442
|
+
v-if="isLoading"
|
|
443
|
+
class="mr-2 h-4 w-4 animate-spin"
|
|
444
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
445
|
+
fill="none"
|
|
446
|
+
viewBox="0 0 24 24"
|
|
447
|
+
>
|
|
448
|
+
<circle
|
|
449
|
+
class="opacity-25"
|
|
450
|
+
cx="12"
|
|
451
|
+
cy="12"
|
|
452
|
+
r="10"
|
|
453
|
+
stroke="currentColor"
|
|
454
|
+
stroke-width="4"
|
|
455
|
+
/>
|
|
456
|
+
<path
|
|
457
|
+
class="opacity-75"
|
|
458
|
+
fill="currentColor"
|
|
459
|
+
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"
|
|
460
|
+
/>
|
|
461
|
+
</svg>
|
|
462
|
+
<slot />
|
|
463
|
+
</button>
|
|
464
|
+
</template>
|
|
465
|
+
|
|
466
|
+
<script setup lang="ts">
|
|
467
|
+
import { computed } from 'vue';
|
|
468
|
+
|
|
469
|
+
interface Props {
|
|
470
|
+
variant?: 'default' | 'destructive' | 'outline' | 'ghost' | 'link';
|
|
471
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
472
|
+
isLoading?: boolean;
|
|
473
|
+
disabled?: boolean;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
477
|
+
variant: 'default',
|
|
478
|
+
size: 'default',
|
|
479
|
+
isLoading: false,
|
|
480
|
+
disabled: false,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const buttonClasses = computed(() => {
|
|
484
|
+
const base = 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
|
|
485
|
+
|
|
486
|
+
const variants = {
|
|
487
|
+
default: 'bg-blue-600 text-white hover:bg-blue-700',
|
|
488
|
+
destructive: 'bg-red-600 text-white hover:bg-red-700',
|
|
489
|
+
outline: 'border border-gray-300 bg-transparent hover:bg-gray-100',
|
|
490
|
+
ghost: 'hover:bg-gray-100',
|
|
491
|
+
link: 'text-blue-600 underline-offset-4 hover:underline',
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const sizes = {
|
|
495
|
+
default: 'h-10 px-4 py-2',
|
|
496
|
+
sm: 'h-9 px-3',
|
|
497
|
+
lg: 'h-11 px-8',
|
|
498
|
+
icon: 'h-10 w-10',
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return \`\${base} \${variants[props.variant]} \${sizes[props.size]}\`;
|
|
502
|
+
});
|
|
503
|
+
</script>
|
|
504
|
+
\`\`\`
|
|
505
|
+
|
|
506
|
+
**使用示例:**
|
|
507
|
+
\`\`\`vue
|
|
508
|
+
<template>
|
|
509
|
+
<div>
|
|
510
|
+
<Button variant="default" size="lg">提交</Button>
|
|
511
|
+
<Button variant="outline" :is-loading="true">加载中...</Button>
|
|
512
|
+
<Button variant="ghost" size="icon">
|
|
513
|
+
<Icon />
|
|
514
|
+
</Button>
|
|
515
|
+
</div>
|
|
516
|
+
</template>
|
|
517
|
+
|
|
518
|
+
<script setup lang="ts">
|
|
519
|
+
import Button from '@/components/Button.vue';
|
|
520
|
+
</script>
|
|
521
|
+
\`\`\`
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
### Vue 3 Input 组件
|
|
526
|
+
\`\`\`vue
|
|
527
|
+
<template>
|
|
528
|
+
<div class="w-full space-y-2">
|
|
529
|
+
<label
|
|
530
|
+
v-if="label"
|
|
531
|
+
:for="inputId"
|
|
532
|
+
class="text-sm font-medium text-gray-700"
|
|
533
|
+
>
|
|
534
|
+
{{ label }}
|
|
535
|
+
<span v-if="required" class="text-red-500 ml-1">*</span>
|
|
536
|
+
</label>
|
|
537
|
+
|
|
538
|
+
<input
|
|
539
|
+
:id="inputId"
|
|
540
|
+
v-model="model"
|
|
541
|
+
:type="type"
|
|
542
|
+
:placeholder="placeholder"
|
|
543
|
+
:required="required"
|
|
544
|
+
:disabled="disabled"
|
|
545
|
+
:aria-invalid="!!error"
|
|
546
|
+
:aria-describedby="error ? \`\${inputId}-error\` : undefined"
|
|
547
|
+
:class="inputClasses"
|
|
548
|
+
v-bind="$attrs"
|
|
549
|
+
/>
|
|
550
|
+
|
|
551
|
+
<p v-if="error" :id="\`\${inputId}-error\`" class="text-sm text-red-600">
|
|
552
|
+
{{ error }}
|
|
553
|
+
</p>
|
|
554
|
+
|
|
555
|
+
<p v-else-if="helperText" class="text-sm text-gray-500">
|
|
556
|
+
{{ helperText }}
|
|
557
|
+
</p>
|
|
558
|
+
</div>
|
|
559
|
+
</template>
|
|
560
|
+
|
|
561
|
+
<script setup lang="ts">
|
|
562
|
+
import { computed, useAttrs } from 'vue';
|
|
563
|
+
|
|
564
|
+
interface Props {
|
|
565
|
+
modelValue?: string | number;
|
|
566
|
+
label?: string;
|
|
567
|
+
type?: string;
|
|
568
|
+
placeholder?: string;
|
|
569
|
+
error?: string;
|
|
570
|
+
helperText?: string;
|
|
571
|
+
required?: boolean;
|
|
572
|
+
disabled?: boolean;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
576
|
+
type: 'text',
|
|
577
|
+
required: false,
|
|
578
|
+
disabled: false,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const emit = defineEmits<{
|
|
582
|
+
'update:modelValue': [value: string | number];
|
|
583
|
+
}>();
|
|
584
|
+
|
|
585
|
+
const attrs = useAttrs();
|
|
586
|
+
const inputId = computed(() => attrs.id as string || \`input-\${Math.random().toString(36).slice(2)}\`);
|
|
587
|
+
|
|
588
|
+
const model = computed({
|
|
589
|
+
get: () => props.modelValue,
|
|
590
|
+
set: (value) => emit('update:modelValue', value),
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const inputClasses = computed(() => {
|
|
594
|
+
return \`
|
|
595
|
+
w-full px-3 py-2 border rounded-md
|
|
596
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500
|
|
597
|
+
disabled:bg-gray-100 disabled:cursor-not-allowed
|
|
598
|
+
\${props.error ? 'border-red-500' : 'border-gray-300'}
|
|
599
|
+
\`;
|
|
600
|
+
});
|
|
601
|
+
</script>
|
|
602
|
+
\`\`\`
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
### Vue 3 Modal 组件
|
|
607
|
+
\`\`\`vue
|
|
608
|
+
<template>
|
|
609
|
+
<Teleport to="body">
|
|
610
|
+
<Transition name="modal">
|
|
611
|
+
<div
|
|
612
|
+
v-if="modelValue"
|
|
613
|
+
class="fixed inset-0 z-50 flex items-center justify-center"
|
|
614
|
+
@click="handleClose"
|
|
615
|
+
>
|
|
616
|
+
<!-- 背景遮罩 -->
|
|
617
|
+
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
|
618
|
+
|
|
619
|
+
<!-- 弹窗内容 -->
|
|
620
|
+
<div
|
|
621
|
+
:class="modalClasses"
|
|
622
|
+
@click.stop
|
|
623
|
+
role="dialog"
|
|
624
|
+
aria-modal="true"
|
|
625
|
+
:aria-labelledby="title ? 'modal-title' : undefined"
|
|
626
|
+
>
|
|
627
|
+
<!-- 头部 -->
|
|
628
|
+
<div v-if="title" class="px-6 py-4 border-b">
|
|
629
|
+
<h2 id="modal-title" class="text-xl font-semibold">
|
|
630
|
+
{{ title }}
|
|
631
|
+
</h2>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<!-- 关闭按钮 -->
|
|
635
|
+
<button
|
|
636
|
+
@click="handleClose"
|
|
637
|
+
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
|
638
|
+
aria-label="关闭"
|
|
639
|
+
>
|
|
640
|
+
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
641
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
642
|
+
</svg>
|
|
643
|
+
</button>
|
|
644
|
+
|
|
645
|
+
<!-- 内容 -->
|
|
646
|
+
<div class="px-6 py-4 overflow-y-auto flex-1">
|
|
647
|
+
<slot />
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</Transition>
|
|
652
|
+
</Teleport>
|
|
653
|
+
</template>
|
|
654
|
+
|
|
655
|
+
<script setup lang="ts">
|
|
656
|
+
import { computed, watch } from 'vue';
|
|
657
|
+
import { onKeyStroke } from '@vueuse/core';
|
|
658
|
+
|
|
659
|
+
interface Props {
|
|
660
|
+
modelValue: boolean;
|
|
661
|
+
title?: string;
|
|
662
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
666
|
+
size: 'md',
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const emit = defineEmits<{
|
|
670
|
+
'update:modelValue': [value: boolean];
|
|
671
|
+
}>();
|
|
672
|
+
|
|
673
|
+
const handleClose = () => {
|
|
674
|
+
emit('update:modelValue', false);
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// ESC 关闭
|
|
678
|
+
onKeyStroke('Escape', () => {
|
|
679
|
+
if (props.modelValue) {
|
|
680
|
+
handleClose();
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// 锁定 body 滚动
|
|
685
|
+
watch(() => props.modelValue, (isOpen) => {
|
|
686
|
+
document.body.style.overflow = isOpen ? 'hidden' : '';
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const modalClasses = computed(() => {
|
|
690
|
+
const sizeClasses = {
|
|
691
|
+
sm: 'max-w-md',
|
|
692
|
+
md: 'max-w-lg',
|
|
693
|
+
lg: 'max-w-2xl',
|
|
694
|
+
xl: 'max-w-4xl',
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
return \`
|
|
698
|
+
relative bg-white rounded-lg shadow-xl
|
|
699
|
+
w-full mx-4 \${sizeClasses[props.size]}
|
|
700
|
+
max-h-[90vh] flex flex-col
|
|
701
|
+
\`;
|
|
702
|
+
});
|
|
703
|
+
</script>
|
|
704
|
+
|
|
705
|
+
<style scoped>
|
|
706
|
+
.modal-enter-active,
|
|
707
|
+
.modal-leave-active {
|
|
708
|
+
transition: opacity 0.3s ease;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.modal-enter-from,
|
|
712
|
+
.modal-leave-to {
|
|
713
|
+
opacity: 0;
|
|
714
|
+
}
|
|
715
|
+
</style>
|
|
716
|
+
\`\`\`
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## 组件库推荐
|
|
721
|
+
|
|
722
|
+
### 🎨 React UI 组件库
|
|
723
|
+
- **shadcn/ui** - 可定制的 React 组件
|
|
724
|
+
- **Radix UI** - 无样式的可访问组件
|
|
725
|
+
- **Headless UI** - Tailwind 官方无样式组件
|
|
726
|
+
- **Ant Design** - 企业级 UI 设计语言
|
|
727
|
+
- **Material-UI** - Google Material Design
|
|
728
|
+
|
|
729
|
+
### 🎨 Vue UI 组件库
|
|
730
|
+
- **Element Plus** - 基于 Vue 3 的企业级组件库
|
|
731
|
+
- **Naive UI** - 完整的 TypeScript 支持
|
|
732
|
+
- **Ant Design Vue** - Vue 版本的 Ant Design
|
|
733
|
+
- **Vuetify** - Material Design 组件框架
|
|
734
|
+
- **PrimeVue** - 丰富的 UI 组件集合
|
|
735
|
+
|
|
736
|
+
### 🎭 动画库
|
|
737
|
+
- **Framer Motion** - React 动画库
|
|
738
|
+
- **React Spring** - 基于物理的动画
|
|
739
|
+
- **GSAP** - 高性能动画引擎(React/Vue 通用)
|
|
740
|
+
- **@vueuse/motion** - Vue 3 组合式动画
|
|
741
|
+
|
|
742
|
+
### 📱 响应式工具
|
|
743
|
+
- **Tailwind CSS** - 实用优先的 CSS 框架
|
|
744
|
+
- **UnoCSS** - 即时原子化 CSS 引擎
|
|
745
|
+
- **CSS Modules** - 局部作用域 CSS
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
现在请根据需求生成完整的 UI 组件代码,包括:
|
|
750
|
+
1. 组件实现(TypeScript)
|
|
751
|
+
2. 样式(Tailwind CSS)
|
|
752
|
+
3. 使用示例
|
|
753
|
+
4. Props 接口定义
|
|
754
|
+
5. 可访问性说明
|
|
755
|
+
|
|
756
|
+
**根据框架选择:**
|
|
757
|
+
- **React**: 使用 Hooks、forwardRef、TypeScript
|
|
758
|
+
- **Vue 3**: 使用 Composition API、script setup、TypeScript
|
|
759
|
+
- **HTML**: 使用原生 JavaScript 和 Web Components`;
|
|
760
|
+
return {
|
|
761
|
+
content: [
|
|
762
|
+
{
|
|
763
|
+
type: "text",
|
|
764
|
+
text: message,
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: "text",
|
|
775
|
+
text: `❌ 生成 UI 组件失败: ${errorMessage}`,
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
isError: true,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|