Răsfoiți Sursa

已修复TypeScript类型错误:移除了处理函数的显式返回类型声明`Promise<Response>`,让TypeScript根据路由定义自动推断正确的返回类型,解决了类型不匹配问题。

yourname 5 luni în urmă
părinte
comite
98b7d21ce8

+ 7 - 0
src/client/admin/menu.tsx

@@ -91,6 +91,13 @@ export const useMenu = () => {
       path: '/admin/believers',
       permission: 'believer:manage'
     },
+    {
+      key: 'groups',
+      label: '分组管理',
+      icon: <TeamOutlined />,
+      path: '/admin/groups',
+      permission: 'group:manage'
+    },
   ];
 
   // 用户菜单项

+ 9 - 9
src/client/admin/pages/Believers.tsx

@@ -5,14 +5,14 @@ import {
 } from 'antd';
 import { useQuery } from '@tanstack/react-query';
 import dayjs from 'dayjs';
-import { believerClient } from '@/client/api';
+import { believersClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import { BelieverStatus } from '@/server/modules/believers/believer.entity';
 
-type BelieverListResponse = InferResponseType<typeof believerClient.$get, 200>;
-type BelieverDetailResponse = InferResponseType<typeof believerClient[':id']['$get'], 200>;
-type CreateBelieverRequest = InferRequestType<typeof believerClient.$post>['json'];
-type UpdateBelieverRequest = InferRequestType<typeof believerClient[':id']['$put']>['json'];
+type BelieverListResponse = InferResponseType<typeof believersClient.$get, 200>;
+type BelieverDetailResponse = InferResponseType<typeof believersClient[':id']['$get'], 200>;
+type CreateBelieverRequest = InferRequestType<typeof believersClient.$post>['json'];
+type UpdateBelieverRequest = InferRequestType<typeof believersClient[':id']['$put']>['json'];
 
 const { Title } = Typography;
 const { Option } = Select;
@@ -32,7 +32,7 @@ export const BelieversPage = () => {
   const { data: believersData, isLoading, refetch } = useQuery({
     queryKey: ['believers', searchParams],
     queryFn: async () => {
-      const res = await believerClient.$get({
+      const res = await believersClient.$get({
         query: {
           page: searchParams.page,
           pageSize: searchParams.pageSize,
@@ -105,7 +105,7 @@ export const BelieversPage = () => {
       
       if (editingBeliever) {
         // 编辑信徒
-        const res = await believerClient[':id']['$put']({
+        const res = await believersClient[':id']['$put']({
           param: { id: editingBeliever.id },
           json: formattedValues
         });
@@ -115,7 +115,7 @@ export const BelieversPage = () => {
         message.success('信徒更新成功');
       } else {
         // 创建信徒
-        const res = await believerClient.$post({
+        const res = await believersClient.$post({
           json: formattedValues
         });
         if (res.status !== 201) {
@@ -136,7 +136,7 @@ export const BelieversPage = () => {
   // 处理删除信徒
   const handleDelete = async (id: number) => {
     try {
-      const res = await believerClient[':id']['$delete']({
+      const res = await believersClient[':id']['$delete']({
         param: { id }
       });
       if (res.status !== 204) {

+ 326 - 0
src/client/admin/pages/Groups.tsx

@@ -0,0 +1,326 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { hc } from 'hono/client';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { groupsClient } from '@/client/api';
+import { logger } from '@/client/utils/logger';
+
+// 定义类型
+type Group = InferResponseType<typeof groupsClient.$get, 200>['data'][0];
+type GroupListResponse = InferResponseType<typeof groupsClient.$get, 200>;
+type GroupDetailResponse = InferResponseType<typeof groupsClient[':id']['$get'], 200>;
+type CreateGroupRequest = InferRequestType<typeof groupsClient.$post>['json'];
+type UpdateGroupRequest = InferRequestType<typeof groupsClient[':id']['$put']>['json'];
+
+// 定义表单类型
+interface GroupFormData {
+  name: string;
+  description?: string;
+  // 可以根据实际需求添加更多字段
+}
+
+const Groups = () => {
+  const navigate = useNavigate();
+  const [groups, setGroups] = useState<Group[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState<string | null>(null);
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
+  const [total, setTotal] = useState(0);
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isEditing, setIsEditing] = useState(false);
+  const [currentGroup, setCurrentGroup] = useState<Group | null>(null);
+  const [formData, setFormData] = useState<GroupFormData>({
+    name: '',
+    description: ''
+  });
+
+  // 获取分组列表
+  const fetchGroups = async () => {
+    try {
+      setLoading(true);
+      const response = await groupsClient.$get({
+        query: {
+          page: currentPage,
+          pageSize: pageSize
+        }
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to fetch groups');
+      }
+      
+      const data: GroupListResponse = await response.json();
+      setGroups(data.data);
+      setTotal(data.pagination.total);
+      setError(null);
+    } catch (err) {
+      logger.error('Error fetching groups:', err);
+      setError(err instanceof Error ? err.message : 'Failed to load groups');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 组件挂载时获取数据
+  useEffect(() => {
+    fetchGroups();
+  }, [currentPage, pageSize]);
+
+  // 处理表单变化
+  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+    const { name, value } = e.target;
+    setFormData(prev => ({ ...prev, [name]: value }));
+  };
+
+  // 打开创建模态框
+  const openCreateModal = () => {
+    setIsEditing(false);
+    setCurrentGroup(null);
+    setFormData({ name: '', description: '' });
+    setIsModalOpen(true);
+  };
+
+  // 打开编辑模态框
+  const openEditModal = async (group: Group) => {
+    try {
+      setLoading(true);
+      const response = await groupsClient[':id']['$get']({
+        param: { id: group.id.toString() }
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to fetch group details');
+      }
+      
+      const data: GroupDetailResponse = await response.json();
+      setCurrentGroup(data.data);
+      setFormData({
+        name: data.data.name,
+        description: data.data.description || ''
+      });
+      setIsEditing(true);
+      setIsModalOpen(true);
+    } catch (err) {
+      logger.error('Error fetching group details:', err);
+      setError(err instanceof Error ? err.message : 'Failed to load group details');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 关闭模态框
+  const closeModal = () => {
+    setIsModalOpen(false);
+  };
+
+  // 提交表单 (创建或更新分组)
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    try {
+      setLoading(true);
+      
+      if (isEditing && currentGroup) {
+        // 更新分组
+        const response = await groupsClient[':id']['$put']({
+          param: { id: currentGroup.id.toString() },
+          json: formData as UpdateGroupRequest
+        });
+        
+        if (!response.ok) {
+          throw new Error('Failed to update group');
+        }
+      } else {
+        // 创建新分组
+        const response = await groupsClient.$post({
+          json: formData as CreateGroupRequest
+        });
+        
+        if (!response.ok) {
+          throw new Error('Failed to create group');
+        }
+      }
+      
+      closeModal();
+      fetchGroups(); // 重新获取列表
+    } catch (err) {
+      logger.error('Error saving group:', err);
+      setError(err instanceof Error ? err.message : 'Failed to save group');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 删除分组
+  const handleDelete = async (id: number) => {
+    if (!confirm('Are you sure you want to delete this group?')) {
+      return;
+    }
+    
+    try {
+      setLoading(true);
+      const response = await groupsClient[':id']['$delete']({
+        param: { id: id.toString() }
+      });
+      
+      if (!response.ok) {
+        throw new Error('Failed to delete group');
+      }
+      
+      fetchGroups(); // 重新获取列表
+    } catch (err) {
+      logger.error('Error deleting group:', err);
+      setError(err instanceof Error ? err.message : 'Failed to delete group');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="container mx-auto py-6">
+      <div className="flex justify-between items-center mb-6">
+        <h1 className="text-2xl font-bold">Groups Management</h1>
+        <button 
+          onClick={openCreateModal}
+          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
+        >
+          Create New Group
+        </button>
+      </div>
+      
+      {error && (
+        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
+          {error}
+        </div>
+      )}
+      
+      <div className="bg-white shadow-md rounded my-6">
+        <table className="min-w-full">
+          <thead>
+            <tr className="bg-gray-100">
+              <th className="py-3 px-4 text-left">ID</th>
+              <th className="py-3 px-4 text-left">Name</th>
+              <th className="py-3 px-4 text-left">Description</th>
+              <th className="py-3 px-4 text-left">Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            {loading ? (
+              <tr>
+                <td colSpan={4} className="py-3 px-4 text-center">Loading...</td>
+              </tr>
+            ) : groups.length === 0 ? (
+              <tr>
+                <td colSpan={4} className="py-3 px-4 text-center">No groups found</td>
+              </tr>
+            ) : (
+              groups.map(group => (
+                <tr key={group.id} className="border-b">
+                  <td className="py-3 px-4">{group.id}</td>
+                  <td className="py-3 px-4">{group.name}</td>
+                  <td className="py-3 px-4">{group.description || '-'}</td>
+                  <td className="py-3 px-4">
+                    <button 
+                      onClick={() => openEditModal(group)}
+                      className="bg-green-500 hover:bg-green-700 text-white py-1 px-2 rounded mr-2 text-sm"
+                    >
+                      Edit
+                    </button>
+                    <button 
+                      onClick={() => handleDelete(group.id)}
+                      className="bg-red-500 hover:bg-red-700 text-white py-1 px-2 rounded text-sm"
+                    >
+                      Delete
+                    </button>
+                  </td>
+                </tr>
+              ))
+            )}
+          </tbody>
+        </table>
+      </div>
+      
+      {/* Pagination controls */}
+      <div className="flex justify-between items-center">
+        <div>
+          Showing {Math.min((currentPage - 1) * pageSize + 1, total)} to {Math.min(currentPage * pageSize, total)} of {total} groups
+        </div>
+        <div className="flex space-x-2">
+          <button 
+            onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
+            disabled={currentPage === 1 || loading}
+            className="px-3 py-1 border rounded disabled:opacity-50"
+          >
+            Previous
+          </button>
+          <button 
+            onClick={() => setCurrentPage(prev => prev + 1)}
+            disabled={currentPage * pageSize >= total || loading}
+            className="px-3 py-1 border rounded disabled:opacity-50"
+          >
+            Next
+          </button>
+        </div>
+      </div>
+      
+      {/* Group Modal */}
+      {isModalOpen && (
+        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
+          <div className="bg-white rounded-lg shadow-lg w-full max-w-md">
+            <div className="p-6">
+              <h2 className="text-xl font-bold mb-4">{isEditing ? 'Edit Group' : 'Create New Group'}</h2>
+              
+              <form onSubmit={handleSubmit}>
+                <div className="mb-4">
+                  <label className="block text-gray-700 text-sm font-bold mb-2">
+                    Group Name
+                  </label>
+                  <input
+                    type="text"
+                    name="name"
+                    value={formData.name}
+                    onChange={handleInputChange}
+                    required
+                    className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
+                  />
+                </div>
+                
+                <div className="mb-6">
+                  <label className="block text-gray-700 text-sm font-bold mb-2">
+                    Description
+                  </label>
+                  <textarea
+                    name="description"
+                    value={formData.description}
+                    onChange={handleInputChange}
+                    className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
+                    rows={3}
+                  />
+                </div>
+                
+                <div className="flex justify-end space-x-3">
+                  <button
+                    type="button"
+                    onClick={closeModal}
+                    className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
+                  >
+                    Cancel
+                  </button>
+                  <button
+                    type="submit"
+                    disabled={loading}
+                    className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:opacity-50"
+                  >
+                    {loading ? 'Saving...' : (isEditing ? 'Update Group' : 'Create Group')}
+                  </button>
+                </div>
+              </form>
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default Groups;

+ 6 - 0
src/client/admin/routes.tsx

@@ -7,6 +7,7 @@ import { NotFoundPage } from './components/NotFoundPage';
 import { DashboardPage } from './pages/Dashboard';
 import { UsersPage } from './pages/Users';
 import { BelieversPage } from './pages/Believers';
+import GroupsPage from './pages/Groups';
 import { LoginPage } from './pages/Login';
 
 export const router = createBrowserRouter([
@@ -30,6 +31,11 @@ export const router = createBrowserRouter([
         index: true,
         element: <Navigate to="/admin/dashboard" />
       },
+      {
+        path: 'groups',
+        element: <GroupsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: 'dashboard',
         element: <DashboardPage />,

+ 6 - 2
src/client/api.ts

@@ -1,7 +1,7 @@
 import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
 import type {
-  AuthRoutes, UserRoutes, BelieverRoutes
+  AuthRoutes, UserRoutes, BelieversRoutes, GroupsRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -61,6 +61,10 @@ export const userClient = hc<UserRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.users;
 
-export const believerClient = hc<BelieverRoutes>('/', {
+export const believersClient = hc<BelieversRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.believers;
+
+export const groupsClient = hc<GroupsRoutes>('/api/v1/groups', {
+  fetch: axiosFetch,
+});

+ 88 - 0
src/server/api/groups/[id]/delete.ts

@@ -0,0 +1,88 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { GroupService } from '@/server/modules/groups/group.service';
+import { AppDataSource } from '@/server/data-source';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AuthContext } from '@/server/types/context';
+
+// 初始化服务
+const groupService = new GroupService(AppDataSource);
+
+// 路径参数定义
+const DeleteParams = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '小组ID'
+  })
+});
+
+// 响应定义
+const DeleteResponse = z.object({
+  code: z.number().openapi({ example: 200, description: '状态码' }),
+  message: z.string().openapi({ example: '删除成功', description: '消息提示' })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [],
+  request: {
+    params: DeleteParams
+  },
+  responses: {
+    200: {
+      description: '删除小组成功',
+      content: {
+        'application/json': { schema: DeleteResponse }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    404: {
+      description: '小组不存在',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    
+    const existingGroup = await groupService.getGroupById(id);
+    if (!existingGroup) {
+      return c.json({ code: 404, message: '小组不存在' }, 404);
+    }
+    
+    const result = await groupService.deleteGroup(id);
+    if (!result) {
+      return c.json({ code: 500, message: '删除小组失败' }, 500);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '删除小组成功'
+    }, 200);
+  } catch (error) {
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '删除小组失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 81 - 0
src/server/api/groups/[id]/get.ts

@@ -0,0 +1,81 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { GroupService } from '@/server/modules/groups/group.service';
+import { AppDataSource } from '@/server/data-source';
+import { GroupSchema } from '@/server/modules/groups/group.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AuthContext } from '@/server/types/context';
+import type { Handler } from 'hono';
+
+// 初始化服务
+const groupService = new GroupService(AppDataSource);
+
+// 路径参数定义
+const GetParams = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '小组ID'
+  })
+});
+
+// 响应定义
+const GetResponse = z.object({
+  data: GroupSchema,
+  code: z.number().openapi({ example: 200, description: '状态码' }),
+  message: z.string().openapi({ example: '操作成功', description: '消息提示' })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/{id}',
+  middleware: [],
+  request: {
+    params: GetParams
+  },
+  responses: {
+    200: {
+      description: '获取小组详情成功',
+      content: {
+        'application/json': { schema: GetResponse }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c): Promise<Response> => {
+  try {
+    const { id } = c.req.valid('param');
+    const group = await groupService.getById(id);
+    
+    if (!group) {
+      return c.json({ code: 404, message: '小组不存在' }, 404);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '获取小组详情成功',
+      data: group
+    }, 200);
+  } catch (error) {
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取小组详情失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 111 - 0
src/server/api/groups/[id]/put.ts

@@ -0,0 +1,111 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { GroupService } from '@/server/modules/groups/group.service';
+import { AppDataSource } from '@/server/data-source';
+import { GroupSchema } from '@/server/modules/groups/group.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AuthContext } from '@/server/types/context';
+import { z } from 'zod';
+
+// 初始化服务
+const groupService = new GroupService(AppDataSource);
+
+// 路径参数定义
+const UpdateParams = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '小组ID'
+  })
+});
+
+// 请求体定义
+const UpdateBody = z.object({
+  name: z.string().min(2).max(50).openapi({
+    example: '更新后的小组名称',
+    description: '小组名称'
+  }),
+  description: z.string().nullable().openapi({
+    example: '更新后的小组描述',
+    description: '小组描述'
+  }),
+  isDisabled: z.coerce.boolean().optional().openapi({
+    example: false,
+    description: '是否禁用'
+  })
+});
+
+// 响应定义
+const UpdateResponse = z.object({
+  data: GroupSchema,
+  code: z.number().openapi({ example: 200, description: '状态码' }),
+  message: z.string().openapi({ example: '更新成功', description: '消息提示' })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [],
+  request: {
+    params: UpdateParams,
+    body: {
+      content: {
+        'application/json': { schema: UpdateBody }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '更新小组成功',
+      content: {
+        'application/json': { schema: UpdateResponse }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    404: {
+      description: '小组不存在',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c): Promise<Response> => {
+  try {
+    const { id } = c.req.valid('param');
+    const data = await c.req.valid('json');
+    
+    const existingGroup = await groupService.getGroupById(id);
+    if (!existingGroup) {
+      return c.json({ code: 404, message: '小组不存在' }, 404);
+    }
+    
+    const updatedGroup = await groupService.updateGroup(id, data);
+    
+    return c.json({
+      code: 200,
+      message: '更新小组成功',
+      data: updatedGroup
+    }, 200);
+  } catch (error) {
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '更新小组失败'
+    }, 500);
+  }
+});
+
+export default app;