Browse Source

♻️ refactor(admin): 重构活动和路线页面数据交互

- 替换原生fetch为RPC客户端(activityClient和routeClient)
- 使用InferResponseType自动生成API响应类型
- 添加统一操作处理函数handleOperation
- 移除冗余的手动接口类型定义
- 增加5分钟数据缓存(staleTime)提升性能

📝 docs(settings): 更新claude配置文件

- 添加pnpm run typecheck:*和pnpm run build命令到允许列表
yourname 4 months ago
parent
commit
642bb3cf64

+ 3 - 1
.claude/settings.local.json

@@ -41,7 +41,9 @@
       "Bash(pnpm db:seed)",
       "Bash(curl:*)",
       "Bash(\"http://localhost:8080/api/v1/admin/activities\")",
-      "Bash(\"http://localhost:8080/api/v1/admin/routes\")"
+      "Bash(\"http://localhost:8080/api/v1/admin/routes\")",
+      "Bash(pnpm run typecheck:*)",
+      "Bash(pnpm run build)"
     ],
     "deny": [],
     "ask": []

+ 32 - 41
src/client/admin/pages/Activities.tsx

@@ -6,64 +6,55 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
 import { DataTablePagination } from '../components/DataTablePagination';
 import { Plus, Edit, Trash2, Calendar } from 'lucide-react';
 import { useState } from 'react';
+import { activityClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
 
-interface Activity {
-  id: number;
-  name: string;
-  description?: string;
-  type: 'departure' | 'return';
-  startDate: string;
-  endDate: string;
-  isDisabled: number;
-  isDeleted: number;
-  createdAt: string;
-  updatedAt: string;
-}
+// 类型提取规范
+type ActivityResponse = InferResponseType<typeof activityClient.$get, 200>['data'][0];
 
-interface ActivitiesResponse {
-  data: Activity[];
-  pagination: {
-    total: number;
-    current: number;
-    pageSize: number;
-  };
-}
+// 统一操作处理函数
+const handleOperation = async (operation: () => Promise<any>) => {
+  try {
+    await operation();
+    // toast.success('操作成功');
+    console.log('操作成功');
+  } catch (error) {
+    console.error('操作失败:', error);
+    // toast.error('操作失败,请重试');
+    throw error;
+  }
+};
 
 export const ActivitiesPage: React.FC = () => {
   const queryClient = useQueryClient();
   const [page, setPage] = useState(1);
   const [pageSize, setPageSize] = useState(20);
 
-  // 获取活动列表
-  const { data, isLoading, error } = useQuery<ActivitiesResponse>({
+  // 获取活动列表 - 使用RPC客户端
+  const { data, isLoading, error } = useQuery({
     queryKey: ['activities', page, pageSize],
     queryFn: async () => {
-      const response = await fetch(`/api/v1/admin/activities?page=${page}&pageSize=${pageSize}`, {
-        headers: {
-          'Authorization': `Bearer ${localStorage.getItem('token')}`,
-          'Content-Type': 'application/json'
+      const res = await activityClient.$get({
+        query: {
+          page,
+          pageSize
         }
       });
-      if (!response.ok) {
-        throw new Error('获取活动列表失败');
-      }
-      return response.json();
+      if (res.status !== 200) throw new Error('获取活动列表失败');
+      return await res.json();
     },
+    staleTime: 5 * 60 * 1000,  // 5分钟缓存
   });
 
-  // 删除活动
+  // 删除活动 - 使用RPC客户端
   const deleteMutation = useMutation({
     mutationFn: async (id: number) => {
-      const response = await fetch(`/api/v1/admin/activities/${id}`, {
-        method: 'DELETE',
-        headers: {
-          'Authorization': `Bearer ${localStorage.getItem('token')}`,
-          'Content-Type': 'application/json'
-        }
+      await handleOperation(async () => {
+        const res = await activityClient[':id'].$delete({
+          param: { id }
+        });
+        if (res.status !== 204) throw new Error('删除活动失败');
       });
-      if (!response.ok) {
-        throw new Error('删除活动失败');
-      }
     },
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ['activities'] });
@@ -128,7 +119,7 @@ export const ActivitiesPage: React.FC = () => {
                     </TableCell>
                   </TableRow>
                 ) : data?.data && data.data.length > 0 ? (
-                  data.data.map((activity) => (
+                  data.data.map((activity: ActivityResponse) => (
                     <TableRow key={activity.id}>
                       <TableCell>
                         <div className="flex items-center gap-2">

+ 32 - 48
src/client/admin/pages/Routes.tsx

@@ -6,71 +6,55 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
 import { DataTablePagination } from '../components/DataTablePagination';
 import { Plus, Edit, Trash2, MapPin, DollarSign, Users } from 'lucide-react';
 import { useState } from 'react';
+import { routeClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
 
-interface Route {
-  id: number;
-  name: string;
-  description?: string;
-  startPoint: string;
-  endPoint: string;
-  pickupPoint: string;
-  dropoffPoint: string;
-  departureTime: string;
-  vehicleType: string;
-  price: number;
-  seatCount: number;
-  availableSeats: number;
-  activityId: number;
-  isDisabled: number;
-  isDeleted: number;
-  createdAt: string;
-  updatedAt: string;
-}
+// 类型提取规范
+type RouteResponse = InferResponseType<typeof routeClient.$get, 200>['data'][0];
 
-interface RoutesResponse {
-  data: Route[];
-  pagination: {
-    total: number;
-    current: number;
-    pageSize: number;
-  };
-}
+// 统一操作处理函数
+const handleOperation = async (operation: () => Promise<any>) => {
+  try {
+    await operation();
+    // toast.success('操作成功');
+    console.log('操作成功');
+  } catch (error) {
+    console.error('操作失败:', error);
+    // toast.error('操作失败,请重试');
+    throw error;
+  }
+};
 
 export const RoutesPage: React.FC = () => {
   const queryClient = useQueryClient();
   const [page, setPage] = useState(1);
   const [pageSize, setPageSize] = useState(20);
 
-  // 获取路线列表
-  const { data, isLoading, error } = useQuery<RoutesResponse>({
+  // 获取路线列表 - 使用RPC客户端
+  const { data, isLoading, error } = useQuery({
     queryKey: ['routes', page, pageSize],
     queryFn: async () => {
-      const response = await fetch(`/api/v1/admin/routes?page=${page}&pageSize=${pageSize}`, {
-        headers: {
-          'Authorization': `Bearer ${localStorage.getItem('token')}`,
-          'Content-Type': 'application/json'
+      const res = await routeClient.$get({
+        query: {
+          page,
+          pageSize
         }
       });
-      if (!response.ok) {
-        throw new Error('获取路线列表失败');
-      }
-      return response.json();
+      if (res.status !== 200) throw new Error('获取路线列表失败');
+      return await res.json();
     },
+    staleTime: 5 * 60 * 1000,  // 5分钟缓存
   });
 
-  // 删除路线
+  // 删除路线 - 使用RPC客户端
   const deleteMutation = useMutation({
     mutationFn: async (id: number) => {
-      const response = await fetch(`/api/v1/admin/routes/${id}`, {
-        method: 'DELETE',
-        headers: {
-          'Authorization': `Bearer ${localStorage.getItem('token')}`,
-          'Content-Type': 'application/json'
-        }
+      await handleOperation(async () => {
+        const res = await routeClient[':id'].$delete({
+          param: { id }
+        });
+        if (res.status !== 204) throw new Error('删除路线失败');
       });
-      if (!response.ok) {
-        throw new Error('删除路线失败');
-      }
     },
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ['routes'] });
@@ -138,7 +122,7 @@ export const RoutesPage: React.FC = () => {
                     </TableCell>
                   </TableRow>
                 ) : data?.data && data.data.length > 0 ? (
-                  data.data.map((route) => (
+                  data.data.map((route: RouteResponse) => (
                     <TableRow key={route.id}>
                       <TableCell>
                         <div className="flex items-center gap-2">

+ 9 - 1
src/client/api.ts

@@ -2,7 +2,7 @@ import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
 import type {
   AuthRoutes, UserRoutes, RoleRoutes,
-  FileRoutes
+  FileRoutes, AdminActivitiesRoutes, AdminRoutesRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -75,3 +75,11 @@ export const roleClient = hc<RoleRoutes>('/', {
 export const fileClient = hc<FileRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.files;
+
+export const activityClient = hc<AdminActivitiesRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.admin.activities;
+
+export const routeClient = hc<AdminRoutesRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.admin.routes;