|
@@ -89,14 +89,12 @@ const exampleUsage = async () => {
|
|
|
|
|
|
|
|
### 3. 管理页面开发
|
|
### 3. 管理页面开发
|
|
|
|
|
|
|
|
-### 3. 页面开发阶段(基于 shadcn-管理页面开发.md)
|
|
|
|
|
-
|
|
|
|
|
#### 文件位置
|
|
#### 文件位置
|
|
|
- 管理页面:`src/client/admin/pages/[EntityName].tsx`
|
|
- 管理页面:`src/client/admin/pages/[EntityName].tsx`
|
|
|
- 类型定义:使用后端API自动提取类型
|
|
- 类型定义:使用后端API自动提取类型
|
|
|
- 表单验证:直接使用后端Zod Schema
|
|
- 表单验证:直接使用后端Zod Schema
|
|
|
|
|
|
|
|
-#### 页面结构
|
|
|
|
|
|
|
+#### 页面组件结构
|
|
|
```typescript
|
|
```typescript
|
|
|
// 1. 类型导入和定义
|
|
// 1. 类型导入和定义
|
|
|
type CreateRequest = InferRequestType<typeof yourEntityClient.$post>['json'];
|
|
type CreateRequest = InferRequestType<typeof yourEntityClient.$post>['json'];
|
|
@@ -134,6 +132,236 @@ export const EntityPage = () => {
|
|
|
};
|
|
};
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+#### 页面布局规范
|
|
|
|
|
+
|
|
|
|
|
+##### 1. 页面标题区域
|
|
|
|
|
+```tsx
|
|
|
|
|
+<div className="flex justify-between items-center">
|
|
|
|
|
+ <h1 className="text-2xl font-bold">页面标题</h1>
|
|
|
|
|
+ <Button onClick={handleCreateEntity}>
|
|
|
|
|
+ <Plus className="mr-2 h-4 w-4" />
|
|
|
|
|
+ 创建实体
|
|
|
|
|
+ </Button>
|
|
|
|
|
+</div>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+##### 2. 搜索区域
|
|
|
|
|
+```tsx
|
|
|
|
|
+<Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle>列表标题</CardTitle>
|
|
|
|
|
+ <CardDescription>列表描述信息</CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="mb-4">
|
|
|
|
|
+ <form onSubmit={handleSearch} className="flex gap-2">
|
|
|
|
|
+ <div className="relative flex-1 max-w-sm">
|
|
|
|
|
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder="搜索提示..."
|
|
|
|
|
+ value={searchParams.search}
|
|
|
|
|
+ onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
|
|
|
|
|
+ className="pl-8"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button type="submit" variant="outline">
|
|
|
|
|
+ 搜索
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+</Card>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+##### 3. 数据表格
|
|
|
|
|
+```tsx
|
|
|
|
|
+<div className="rounded-md border">
|
|
|
|
|
+ <Table>
|
|
|
|
|
+ <TableHeader>
|
|
|
|
|
+ <TableRow>
|
|
|
|
|
+ <TableHead>列标题1</TableHead>
|
|
|
|
|
+ <TableHead>列标题2</TableHead>
|
|
|
|
|
+ <TableHead className="text-right">操作</TableHead>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ </TableHeader>
|
|
|
|
|
+ <TableBody>
|
|
|
|
|
+ {data.map((item) => (
|
|
|
|
|
+ <TableRow key={item.id}>
|
|
|
|
|
+ <TableCell>{item.field1}</TableCell>
|
|
|
|
|
+ <TableCell>{item.field2}</TableCell>
|
|
|
|
|
+ <TableCell className="text-right">
|
|
|
|
|
+ <div className="flex justify-end gap-2">
|
|
|
|
|
+ <Button variant="ghost" size="icon" onClick={() => handleEdit(item)}>
|
|
|
|
|
+ <Edit className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button variant="ghost" size="icon" onClick={() => handleDelete(item.id)}>
|
|
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </TableBody>
|
|
|
|
|
+ </Table>
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+{data?.data.length === 0 && !isLoading && (
|
|
|
|
|
+ <div className="text-center py-8">
|
|
|
|
|
+ <p className="text-muted-foreground">暂无数据</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+)}
|
|
|
|
|
+
|
|
|
|
|
+<DataTablePagination
|
|
|
|
|
+ currentPage={searchParams.page}
|
|
|
|
|
+ pageSize={searchParams.limit}
|
|
|
|
|
+ totalCount={data?.pagination.total || 0}
|
|
|
|
|
+ onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
|
|
|
|
|
+/>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 高级组件集成
|
|
|
|
|
+
|
|
|
|
|
+##### 1. DataTablePagination 分页组件
|
|
|
|
|
+```tsx
|
|
|
|
|
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
|
|
|
|
|
+
|
|
|
|
|
+<DataTablePagination
|
|
|
|
|
+ currentPage={searchParams.page}
|
|
|
|
|
+ pageSize={searchParams.limit}
|
|
|
|
|
+ totalCount={data?.pagination.total || 0}
|
|
|
|
|
+ onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
|
|
|
|
|
+/>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+##### 2. 关联实体Selector组件
|
|
|
|
|
+```tsx
|
|
|
|
|
+import AdvertisementTypeSelector from '@/client/admin/components/AdvertisementTypeSelector';
|
|
|
|
|
+
|
|
|
|
|
+<FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="typeId"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>广告类型</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <AdvertisementTypeSelector
|
|
|
|
|
+ value={field.value}
|
|
|
|
|
+ onChange={field.onChange}
|
|
|
|
|
+ placeholder="请选择广告类型"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+/>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+##### 3. 图片选择器集成
|
|
|
|
|
+```tsx
|
|
|
|
|
+import ImageSelector from '@/client/admin/components/ImageSelector';
|
|
|
|
|
+
|
|
|
|
|
+<FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="imageFileId"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>广告图片</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <ImageSelector
|
|
|
|
|
+ value={field.value || undefined}
|
|
|
|
|
+ onChange={field.onChange}
|
|
|
|
|
+ maxSize={2} // MB
|
|
|
|
|
+ uploadPath="/advertisements"
|
|
|
|
|
+ uploadButtonText="上传广告图片"
|
|
|
|
|
+ previewSize="medium"
|
|
|
|
|
+ placeholder="选择广告图片"
|
|
|
|
|
+ title="选择广告图片"
|
|
|
|
|
+ description="上传新图片或从已有图片中选择"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+/>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 复杂字段展示模式
|
|
|
|
|
+
|
|
|
|
|
+##### 1. 关联实体字段展示
|
|
|
|
|
+```tsx
|
|
|
|
|
+<TableCell>
|
|
|
|
|
+ {advertisement.advertisementType?.name || '-'}
|
|
|
|
|
+</TableCell>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+##### 2. 状态字段展示
|
|
|
|
|
+```tsx
|
|
|
|
|
+<TableCell>
|
|
|
|
|
+ <Badge variant={advertisement.status === 1 ? 'default' : 'secondary'}>
|
|
|
|
|
+ {advertisement.status === 1 ? '启用' : '禁用'}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+</TableCell>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+##### 3. 图片字段展示
|
|
|
|
|
+```tsx
|
|
|
|
|
+<TableCell>
|
|
|
|
|
+ {advertisement.imageFile?.fullUrl ? (
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={advertisement.imageFile.fullUrl}
|
|
|
|
|
+ alt={advertisement.title || '图片'}
|
|
|
|
|
+ className="w-16 h-10 object-cover rounded"
|
|
|
|
|
+ onError={(e) => {
|
|
|
|
|
+ e.currentTarget.src = '/placeholder.png';
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className="text-muted-foreground text-xs">无图片</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+</TableCell>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 表单字段类型映射
|
|
|
|
|
+
|
|
|
|
|
+| 字段类型 | 组件 | 示例 |
|
|
|
|
|
+|----------|------|------|
|
|
|
|
|
+| 文本输入 | Input | `<Input placeholder="请输入标题" {...field} />` |
|
|
|
|
|
+| 长文本 | Textarea | `<Textarea placeholder="请输入描述" {...field} />` |
|
|
|
|
|
+| 选择器 | Select | `<Select value={field.value} onValueChange={field.onChange}>` |
|
|
|
|
|
+| 数字输入 | Input | `<Input type="number" {...field} />` |
|
|
|
|
|
+| 日期选择 | DatePicker | `<DatePicker selected={field.value} onChange={field.onChange} />` |
|
|
|
|
|
+| 开关 | Switch | `<Switch checked={field.value} onCheckedChange={field.onChange} />` |
|
|
|
|
|
+| 文件上传 | ImageSelector | `<ImageSelector value={field.value} onChange={field.onChange} />` |
|
|
|
|
|
+
|
|
|
|
|
+#### 日期格式化规范
|
|
|
|
|
+```tsx
|
|
|
|
|
+import { format } from 'date-fns';
|
|
|
|
|
+
|
|
|
|
|
+// 标准日期时间格式:yyyy-MM-dd HH:mm
|
|
|
|
|
+<TableCell>
|
|
|
|
|
+ {user.createdAt ? format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm') : '-'}
|
|
|
|
|
+</TableCell>
|
|
|
|
|
+
|
|
|
|
|
+// 仅日期格式:yyyy-MM-dd
|
|
|
|
|
+<TableCell>
|
|
|
|
|
+ {user.birthday ? format(new Date(user.birthday), 'yyyy-MM-dd') : '-'}
|
|
|
|
|
+</TableCell>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 消息通知规范
|
|
|
|
|
+```typescript
|
|
|
|
|
+import { toast } from 'sonner';
|
|
|
|
|
+
|
|
|
|
|
+// 成功通知
|
|
|
|
|
+toast.success('操作成功');
|
|
|
|
|
+toast.success('用户创建成功');
|
|
|
|
|
+
|
|
|
|
|
+// 错误通知
|
|
|
|
|
+toast.error('操作失败');
|
|
|
|
|
+toast.error('创建用户失败,请重试');
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
#### 核心开发模式
|
|
#### 核心开发模式
|
|
|
|
|
|
|
|
##### 类型驱动的开发
|
|
##### 类型驱动的开发
|