|
|
@@ -1,5 +1,5 @@
|
|
|
import React, { useState } from 'react';
|
|
|
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
+import { useQuery, useMutation } from '@tanstack/react-query';
|
|
|
import { Plus, Edit, Trash2, Search } from 'lucide-react';
|
|
|
import { format } from 'date-fns';
|
|
|
import { Input } from '@d8d/shared-ui-components/components/ui/input';
|
|
|
@@ -7,6 +7,7 @@ import { Button } from '@d8d/shared-ui-components/components/ui/button';
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
|
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
|
|
|
import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
|
|
|
+import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
|
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
|
|
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
|
|
|
import { useForm } from 'react-hook-form';
|
|
|
@@ -15,20 +16,17 @@ import { toast } from 'sonner';
|
|
|
import { DataTablePagination } from '@d8d/shared-ui-components/components/admin/DataTablePagination';
|
|
|
import { FileSelector } from '@d8d/file-management-ui';
|
|
|
import { AdvertisementTypeSelector } from '@d8d/advertisement-type-management-ui';
|
|
|
-import { getAdvertisementClient } from '../api/advertisementClient';
|
|
|
-import type { InferRequestType, InferResponseType } from 'hono/client';
|
|
|
+import { advertisementClientManager } from '../api/advertisementClient';
|
|
|
import { CreateAdvertisementDto, UpdateAdvertisementDto } from '@d8d/advertisements-module/schemas';
|
|
|
-import type { AdvertisementSearchParams } from '../types';
|
|
|
+import type { AdvertisementSearchParams, CreateAdvertisementRequest, UpdateAdvertisementRequest, AdvertisementResponse } from '../types';
|
|
|
|
|
|
-type CreateRequest = InferRequestType<ReturnType<typeof getAdvertisementClient>['$post']>['json'];
|
|
|
-type UpdateRequest = InferRequestType<ReturnType<typeof getAdvertisementClient>[':id']['$put']>['json'];
|
|
|
-type AdvertisementResponse = InferResponseType<ReturnType<typeof getAdvertisementClient>['$get'], 200>['data'][0];
|
|
|
+type CreateRequest = CreateAdvertisementRequest;
|
|
|
+type UpdateRequest = UpdateAdvertisementRequest;
|
|
|
|
|
|
const createFormSchema = CreateAdvertisementDto;
|
|
|
const updateFormSchema = UpdateAdvertisementDto;
|
|
|
|
|
|
export const AdvertisementManagement: React.FC = () => {
|
|
|
- const queryClient = useQueryClient();
|
|
|
const [searchParams, setSearchParams] = useState<AdvertisementSearchParams>({ page: 1, limit: 10, search: '' });
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
const [editingAdvertisement, setEditingAdvertisement] = useState<AdvertisementResponse | null>(null);
|
|
|
@@ -60,8 +58,7 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
const { data, isLoading, refetch } = useQuery({
|
|
|
queryKey: ['advertisements', searchParams],
|
|
|
queryFn: async () => {
|
|
|
- const client = getAdvertisementClient();
|
|
|
- const res = await client.$get({
|
|
|
+ const res = await advertisementClientManager.get().index.$get({
|
|
|
query: {
|
|
|
page: searchParams.page,
|
|
|
pageSize: searchParams.limit,
|
|
|
@@ -76,8 +73,7 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
// 创建广告
|
|
|
const createMutation = useMutation({
|
|
|
mutationFn: async (data: CreateRequest) => {
|
|
|
- const client = getAdvertisementClient();
|
|
|
- const res = await client.$post({ json: data });
|
|
|
+ const res = await advertisementClientManager.get().index.$post({ json: data });
|
|
|
if (res.status !== 201) throw new Error('创建广告失败');
|
|
|
return await res.json();
|
|
|
},
|
|
|
@@ -95,9 +91,8 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
// 更新广告
|
|
|
const updateMutation = useMutation({
|
|
|
mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
|
|
|
- const client = getAdvertisementClient();
|
|
|
- const res = await client[':id'].$put({
|
|
|
- param: { id: id.toString() },
|
|
|
+ const res = await advertisementClientManager.get()[':id']['$put']({
|
|
|
+ param: { id },
|
|
|
json: data
|
|
|
});
|
|
|
if (res.status !== 200) throw new Error('更新广告失败');
|
|
|
@@ -117,9 +112,8 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
// 删除广告
|
|
|
const deleteMutation = useMutation({
|
|
|
mutationFn: async (id: number) => {
|
|
|
- const client = getAdvertisementClient();
|
|
|
- const res = await client[':id'].$delete({
|
|
|
- param: { id: id.toString() }
|
|
|
+ const res = await advertisementClientManager.get()[':id']['$delete']({
|
|
|
+ param: { id }
|
|
|
});
|
|
|
if (res.status !== 204) throw new Error('删除广告失败');
|
|
|
return await res.json();
|
|
|
@@ -203,30 +197,6 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // 渲染加载骨架
|
|
|
- if (isLoading) {
|
|
|
- return (
|
|
|
- <div className="space-y-4">
|
|
|
- <div className="flex justify-between items-center">
|
|
|
- <h1 className="text-2xl font-bold">广告管理</h1>
|
|
|
- <Button disabled>
|
|
|
- <Plus className="mr-2 h-4 w-4" />
|
|
|
- 创建广告
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <Card>
|
|
|
- <CardHeader>
|
|
|
- <div className="h-6 w-1/4 bg-muted animate-pulse rounded" />
|
|
|
- </CardHeader>
|
|
|
- <CardContent>
|
|
|
- <div className="h-32 w-full bg-muted animate-pulse rounded" />
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
<div className="space-y-4">
|
|
|
<div className="flex justify-between items-center">
|
|
|
@@ -262,97 +232,137 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded-md border">
|
|
|
- <Table>
|
|
|
- <TableHeader>
|
|
|
- <TableRow>
|
|
|
- <TableHead>ID</TableHead>
|
|
|
- <TableHead>标题</TableHead>
|
|
|
- <TableHead>类型</TableHead>
|
|
|
- <TableHead>别名</TableHead>
|
|
|
- <TableHead>图片</TableHead>
|
|
|
- <TableHead>状态</TableHead>
|
|
|
- <TableHead>排序</TableHead>
|
|
|
- <TableHead>创建时间</TableHead>
|
|
|
- <TableHead className="text-right">操作</TableHead>
|
|
|
- </TableRow>
|
|
|
- </TableHeader>
|
|
|
- <TableBody>
|
|
|
- {data?.data.map((advertisement) => (
|
|
|
- <TableRow key={advertisement.id}>
|
|
|
- <TableCell>{advertisement.id}</TableCell>
|
|
|
- <TableCell>{advertisement.title || '-'}</TableCell>
|
|
|
- <TableCell>
|
|
|
- {advertisement.advertisementType?.name || '-'}
|
|
|
- </TableCell>
|
|
|
- <TableCell>
|
|
|
- <code className="text-xs bg-muted px-1 rounded">{advertisement.code || '-'}</code>
|
|
|
- </TableCell>
|
|
|
- <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>
|
|
|
- <TableCell>
|
|
|
- <Badge variant={advertisement.status === 1 ? 'default' : 'secondary'}>
|
|
|
- {advertisement.status === 1 ? '启用' : '禁用'}
|
|
|
- </Badge>
|
|
|
- </TableCell>
|
|
|
- <TableCell>{advertisement.sort}</TableCell>
|
|
|
- <TableCell>
|
|
|
- {advertisement.createdAt ? format(new Date(advertisement.createdAt), 'yyyy-MM-dd HH:mm') : '-'}
|
|
|
- </TableCell>
|
|
|
- <TableCell className="text-right">
|
|
|
- <div className="flex justify-end gap-2">
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="icon"
|
|
|
- onClick={() => handleEditAdvertisement(advertisement)}
|
|
|
- data-testid={`edit-button-${advertisement.id}`}
|
|
|
- >
|
|
|
- <Edit className="h-4 w-4" />
|
|
|
- </Button>
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="icon"
|
|
|
- onClick={() => handleDeleteAdvertisement(advertisement.id)}
|
|
|
- data-testid={`delete-button-${advertisement.id}`}
|
|
|
- >
|
|
|
- <Trash2 className="h-4 w-4" />
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </TableCell>
|
|
|
+ <div className="relative w-full overflow-x-auto">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>ID</TableHead>
|
|
|
+ <TableHead>标题</TableHead>
|
|
|
+ <TableHead>类型</TableHead>
|
|
|
+ <TableHead>别名</TableHead>
|
|
|
+ <TableHead>图片</TableHead>
|
|
|
+ <TableHead>状态</TableHead>
|
|
|
+ <TableHead>排序</TableHead>
|
|
|
+ <TableHead>创建时间</TableHead>
|
|
|
+ <TableHead className="text-right">操作</TableHead>
|
|
|
</TableRow>
|
|
|
- ))}
|
|
|
- </TableBody>
|
|
|
- </Table>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {isLoading ? (
|
|
|
+ Array.from({ length: 5 }).map((_, index) => (
|
|
|
+ <TableRow key={index}>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-4 w-8" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-4 w-32" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-4 w-20" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-4 w-24" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-8 w-8 rounded" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-6 w-12 rounded-full" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-4 w-8" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Skeleton className="h-4 w-24" />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <div className="flex justify-end gap-2">
|
|
|
+ <Skeleton className="h-8 w-8 rounded" />
|
|
|
+ <Skeleton className="h-8 w-8 rounded" />
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))
|
|
|
+ ) : data?.data && data.data.length > 0 ? (
|
|
|
+ data.data.map((advertisement) => (
|
|
|
+ <TableRow key={advertisement.id}>
|
|
|
+ <TableCell>{advertisement.id}</TableCell>
|
|
|
+ <TableCell>{advertisement.title || '-'}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {advertisement.advertisementType?.name || '-'}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <code className="text-xs bg-muted px-1 rounded">{advertisement.code || '-'}</code>
|
|
|
+ </TableCell>
|
|
|
+ <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>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant={advertisement.status === 1 ? 'default' : 'secondary'}>
|
|
|
+ {advertisement.status === 1 ? '启用' : '禁用'}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{advertisement.sort}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {advertisement.createdAt ? format(new Date(advertisement.createdAt), 'yyyy-MM-dd HH:mm') : '-'}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="text-right">
|
|
|
+ <div className="flex justify-end gap-2">
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="icon"
|
|
|
+ onClick={() => handleEditAdvertisement(advertisement)}
|
|
|
+ data-testid={`edit-button-${advertisement.id}`}
|
|
|
+ >
|
|
|
+ <Edit className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="icon"
|
|
|
+ onClick={() => handleDeleteAdvertisement(advertisement.id)}
|
|
|
+ data-testid={`delete-button-${advertisement.id}`}
|
|
|
+ >
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))
|
|
|
+ ) : (
|
|
|
+ <TableRow>
|
|
|
+ <TableCell colSpan={9} className="text-center py-8">
|
|
|
+ <p className="text-muted-foreground">暂无广告数据</p>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ )}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
</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 }))}
|
|
|
- />
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
-
|
|
|
- {/* 创建/编辑对话框 */}
|
|
|
- <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
|
+ <DataTablePagination
|
|
|
+ currentPage={searchParams.page}
|
|
|
+ pageSize={searchParams.limit}
|
|
|
+ totalCount={data?.pagination.total || 0}
|
|
|
+ onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
|
|
|
+ />
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* 创建/编辑对话框 */}
|
|
|
+ <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
|
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
|
|
<DialogHeader>
|
|
|
<DialogTitle>{isCreateForm ? '创建广告' : '编辑广告'}</DialogTitle>
|
|
|
@@ -432,11 +442,11 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
onChange={field.onChange}
|
|
|
maxSize={2}
|
|
|
uploadPath="/advertisements"
|
|
|
- uploadButtonText="上传广告图片"
|
|
|
previewSize="medium"
|
|
|
placeholder="选择广告图片"
|
|
|
title="选择广告图片"
|
|
|
description="上传新图片或从已有图片中选择"
|
|
|
+ filterType="image"
|
|
|
/>
|
|
|
</FormControl>
|
|
|
<FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
|
|
|
@@ -610,11 +620,11 @@ export const AdvertisementManagement: React.FC = () => {
|
|
|
onChange={field.onChange}
|
|
|
maxSize={2}
|
|
|
uploadPath="/advertisements"
|
|
|
- uploadButtonText="上传广告图片"
|
|
|
previewSize="medium"
|
|
|
placeholder="选择广告图片"
|
|
|
title="选择广告图片"
|
|
|
description="上传新图片或从已有图片中选择"
|
|
|
+ filterType="image"
|
|
|
/>
|
|
|
</FormControl>
|
|
|
<FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
|