|
|
@@ -1,17 +1,13 @@
|
|
|
import { useState } from 'react';
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
-import { useForm } from 'react-hook-form';
|
|
|
-import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
import { Plus, Search, Edit, Trash2, MapPin } from 'lucide-react';
|
|
|
import { Button } from '@/client/components/ui/button';
|
|
|
import { Input } from '@/client/components/ui/input';
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
|
|
|
import { Badge } from '@/client/components/ui/badge';
|
|
|
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/client/components/ui/dialog';
|
|
|
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/client/components/ui/dialog';
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
|
|
|
-import { Textarea } from '@/client/components/ui/textarea';
|
|
|
import { toast } from 'sonner';
|
|
|
import { locationClient, areaClient } from '@/client/api';
|
|
|
import { LocationForm } from '../components/LocationForm';
|
|
|
@@ -20,13 +16,23 @@ interface Location {
|
|
|
id: number;
|
|
|
name: string;
|
|
|
address: string;
|
|
|
- area: {
|
|
|
+ province: {
|
|
|
id: number;
|
|
|
name: string;
|
|
|
code: string;
|
|
|
- };
|
|
|
- latitude?: number;
|
|
|
- longitude?: number;
|
|
|
+ } | null;
|
|
|
+ city: {
|
|
|
+ id: number;
|
|
|
+ name: string;
|
|
|
+ code: string;
|
|
|
+ } | null;
|
|
|
+ district: {
|
|
|
+ id: number;
|
|
|
+ name: string;
|
|
|
+ code: string;
|
|
|
+ } | null;
|
|
|
+ latitude?: number | null;
|
|
|
+ longitude?: number | null;
|
|
|
isDisabled: number;
|
|
|
createdAt: string;
|
|
|
updatedAt: string;
|
|
|
@@ -55,62 +61,81 @@ export const LocationsPage = () => {
|
|
|
// 获取地点列表
|
|
|
const { data: locationsData, isLoading } = useQuery({
|
|
|
queryKey: ['locations', searchParams],
|
|
|
- queryFn: () => locationClient.$get({ query: searchParams }),
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await locationClient.$get({ query: searchParams });
|
|
|
+ if (res.status !== 200) throw new Error('获取地点列表失败');
|
|
|
+ return await res.json();
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
// 获取区域列表
|
|
|
const { data: areasData } = useQuery({
|
|
|
queryKey: ['areas'],
|
|
|
- queryFn: () => areaClient.$get({ query: { pageSize: 100 } }),
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await areaClient.$get({ query: { pageSize: 100 } });
|
|
|
+ if (res.status !== 200) throw new Error('获取区域列表失败');
|
|
|
+ return await res.json();
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
// 创建地点
|
|
|
const createMutation = useMutation({
|
|
|
- mutationFn: (data: any) => locationClient.$post({ json: data }),
|
|
|
+ mutationFn: async (data: any) => {
|
|
|
+ const res = await locationClient.$post({ json: data });
|
|
|
+ if (res.status !== 201) throw new Error('创建地点失败');
|
|
|
+ },
|
|
|
onSuccess: () => {
|
|
|
queryClient.invalidateQueries({ queryKey: ['locations'] });
|
|
|
setIsCreateDialogOpen(false);
|
|
|
toast.success('地点已成功创建');
|
|
|
},
|
|
|
- onError: (error: any) => {
|
|
|
+ onError: (error: Error) => {
|
|
|
toast.error(error.message || '创建地点失败');
|
|
|
},
|
|
|
});
|
|
|
|
|
|
// 更新地点
|
|
|
const updateMutation = useMutation({
|
|
|
- mutationFn: ({ id, data }: { id: number; data: any }) => locationClient[':id'].$put({ param: { id }, json: data }),
|
|
|
+ mutationFn: async ({ id, data }: { id: number; data: any }) => {
|
|
|
+ const res = await locationClient[':id'].$put({ param: { id }, json: data });
|
|
|
+ if (res.status !== 200) throw new Error('更新地点失败');
|
|
|
+ },
|
|
|
onSuccess: () => {
|
|
|
queryClient.invalidateQueries({ queryKey: ['locations'] });
|
|
|
setEditingLocation(null);
|
|
|
toast.success('地点已成功更新');
|
|
|
},
|
|
|
- onError: (error: any) => {
|
|
|
+ onError: (error: Error) => {
|
|
|
toast.error(error.message || '更新地点失败');
|
|
|
},
|
|
|
});
|
|
|
|
|
|
// 删除地点
|
|
|
const deleteMutation = useMutation({
|
|
|
- mutationFn: (id: number) => locationClient[':id'].$delete({ param: { id } }),
|
|
|
+ mutationFn: async (id: number) => {
|
|
|
+ const res = await locationClient[':id'].$delete({ param: { id } });
|
|
|
+ if (res.status !== 204) throw new Error('删除地点失败');
|
|
|
+ },
|
|
|
onSuccess: () => {
|
|
|
queryClient.invalidateQueries({ queryKey: ['locations'] });
|
|
|
toast.success('地点已成功删除');
|
|
|
},
|
|
|
- onError: (error: any) => {
|
|
|
+ onError: (error: Error) => {
|
|
|
toast.error(error.message || '删除地点失败');
|
|
|
},
|
|
|
});
|
|
|
|
|
|
// 切换状态
|
|
|
const toggleStatusMutation = useMutation({
|
|
|
- mutationFn: ({ id, isDisabled }: { id: number; isDisabled: number }) =>
|
|
|
- locationClient[':id'].status.$patch({ param: { id }, json: { isDisabled } }),
|
|
|
+ mutationFn: async ({ id, isDisabled }: { id: number; isDisabled: number }) => {
|
|
|
+ const res = await locationClient[':id'].$put({ param: { id }, json: { isDisabled } });
|
|
|
+ if (res.status !== 200) throw new Error('更新状态失败');
|
|
|
+ },
|
|
|
onSuccess: () => {
|
|
|
queryClient.invalidateQueries({ queryKey: ['locations'] });
|
|
|
toast.success('地点状态已更新');
|
|
|
},
|
|
|
- onError: (error: any) => {
|
|
|
+ onError: (error: Error) => {
|
|
|
toast.error(error.message || '更新状态失败');
|
|
|
},
|
|
|
});
|
|
|
@@ -126,7 +151,7 @@ export const LocationsPage = () => {
|
|
|
const handleAreaFilter = (areaId: string) => {
|
|
|
setSearchParams(prev => ({
|
|
|
...prev,
|
|
|
- areaId: areaId ? parseInt(areaId) : undefined,
|
|
|
+ areaId: areaId && areaId !== 'all' ? parseInt(areaId) : undefined,
|
|
|
page: 1,
|
|
|
}));
|
|
|
};
|
|
|
@@ -134,7 +159,7 @@ export const LocationsPage = () => {
|
|
|
const handleStatusFilter = (status: string) => {
|
|
|
setSearchParams(prev => ({
|
|
|
...prev,
|
|
|
- isDisabled: status ? parseInt(status) : undefined,
|
|
|
+ isDisabled: status && status !== 'all' ? parseInt(status) : undefined,
|
|
|
page: 1,
|
|
|
}));
|
|
|
};
|
|
|
@@ -164,8 +189,8 @@ export const LocationsPage = () => {
|
|
|
setEditingLocation(location);
|
|
|
};
|
|
|
|
|
|
- const locations = locationsData?.data?.data || [];
|
|
|
- const pagination = locationsData?.data?.pagination;
|
|
|
+ const locations = locationsData?.data || [];
|
|
|
+ const pagination = locationsData?.pagination;
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-6">
|
|
|
@@ -193,7 +218,7 @@ export const LocationsPage = () => {
|
|
|
<LocationForm
|
|
|
onSubmit={handleCreate}
|
|
|
isLoading={createMutation.isPending}
|
|
|
- areas={areasData?.data?.data || []}
|
|
|
+ areas={areasData?.data || []}
|
|
|
/>
|
|
|
</DialogContent>
|
|
|
</Dialog>
|
|
|
@@ -225,8 +250,8 @@ export const LocationsPage = () => {
|
|
|
<SelectValue placeholder="选择区域" />
|
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
|
- <SelectItem value="">全部区域</SelectItem>
|
|
|
- {areasData?.data?.data?.map((area: any) => (
|
|
|
+ <SelectItem value="all">全部区域</SelectItem>
|
|
|
+ {areasData?.data?.map((area: any) => (
|
|
|
<SelectItem key={area.id} value={area.id.toString()}>
|
|
|
{area.name}
|
|
|
</SelectItem>
|
|
|
@@ -238,7 +263,7 @@ export const LocationsPage = () => {
|
|
|
<SelectValue placeholder="状态筛选" />
|
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
|
- <SelectItem value="">全部状态</SelectItem>
|
|
|
+ <SelectItem value="all">全部状态</SelectItem>
|
|
|
<SelectItem value="0">启用</SelectItem>
|
|
|
<SelectItem value="1">禁用</SelectItem>
|
|
|
</SelectContent>
|
|
|
@@ -281,9 +306,23 @@ export const LocationsPage = () => {
|
|
|
<TableCell className="font-medium">{location.name}</TableCell>
|
|
|
<TableCell>{location.address}</TableCell>
|
|
|
<TableCell>
|
|
|
- <Badge variant="outline">
|
|
|
- {location.area?.name}
|
|
|
- </Badge>
|
|
|
+ <div className="flex flex-col gap-1">
|
|
|
+ {location.province && (
|
|
|
+ <Badge variant="outline">
|
|
|
+ {location.province.name}
|
|
|
+ </Badge>
|
|
|
+ )}
|
|
|
+ {location.city && (
|
|
|
+ <Badge variant="outline">
|
|
|
+ {location.city.name}
|
|
|
+ </Badge>
|
|
|
+ )}
|
|
|
+ {location.district && (
|
|
|
+ <Badge variant="outline">
|
|
|
+ {location.district.name}
|
|
|
+ </Badge>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</TableCell>
|
|
|
<TableCell>
|
|
|
{location.latitude && location.longitude ? (
|
|
|
@@ -338,15 +377,15 @@ export const LocationsPage = () => {
|
|
|
{pagination && pagination.total > 0 && (
|
|
|
<div className="flex items-center justify-between">
|
|
|
<div className="text-sm text-muted-foreground">
|
|
|
- 显示第 {(pagination.page - 1) * pagination.pageSize + 1} 到{' '}
|
|
|
- {Math.min(pagination.page * pagination.pageSize, pagination.total)} 条,
|
|
|
+ 显示第 {(pagination.current - 1) * pagination.pageSize + 1} 到{' '}
|
|
|
+ {Math.min(pagination.current * pagination.pageSize, pagination.total)} 条,
|
|
|
共 {pagination.total} 条记录
|
|
|
</div>
|
|
|
<div className="flex gap-2">
|
|
|
<Button
|
|
|
variant="outline"
|
|
|
size="sm"
|
|
|
- disabled={pagination.page <= 1}
|
|
|
+ disabled={pagination.current <= 1}
|
|
|
onClick={() => setSearchParams(prev => ({ ...prev, page: prev.page! - 1 }))}
|
|
|
>
|
|
|
上一页
|
|
|
@@ -354,7 +393,7 @@ export const LocationsPage = () => {
|
|
|
<Button
|
|
|
variant="outline"
|
|
|
size="sm"
|
|
|
- disabled={pagination.page >= pagination.totalPages}
|
|
|
+ disabled={pagination.current >= Math.ceil(pagination.total / pagination.pageSize)}
|
|
|
onClick={() => setSearchParams(prev => ({ ...prev, page: prev.page! + 1 }))}
|
|
|
>
|
|
|
下一页
|
|
|
@@ -379,7 +418,7 @@ export const LocationsPage = () => {
|
|
|
<LocationForm
|
|
|
onSubmit={handleUpdate}
|
|
|
isLoading={updateMutation.isPending}
|
|
|
- areas={areasData?.data?.data || []}
|
|
|
+ areas={areasData?.data || []}
|
|
|
initialData={editingLocation}
|
|
|
/>
|
|
|
)}
|