瀏覽代碼

已完成信徒管理相关页面和API的开发,包括:

后端API:

创建信徒详情、更新和删除路由文件
完善信徒API路由聚合
前端页面:

实现信徒管理页面(Believers.tsx),包含列表展示、搜索、分页和CRUD操作
添加菜单和路由配置,使信徒管理页面可访问
功能特点:

信徒信息的增删改查功能
状态标签可视化展示
日期选择器支持
表单验证和错误处理
所有代码遵循项目规范,包括类型安全、错误处理和API文档定义。
yourname 5 月之前
父節點
當前提交
e2978f796c

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

@@ -6,6 +6,7 @@ import {
   DashboardOutlined,
   TeamOutlined,
   InfoCircleOutlined,
+  UserAddOutlined,
 } from '@ant-design/icons';
 
 export interface MenuItem {
@@ -83,6 +84,13 @@ export const useMenu = () => {
       path: '/admin/users',
       permission: 'user:manage'
     },
+    {
+      key: 'believers',
+      label: '信徒管理',
+      icon: <UserAddOutlined />,
+      path: '/admin/believers',
+      permission: 'believer:manage'
+    },
   ];
 
   // 用户菜单项

+ 352 - 0
src/client/admin/pages/Believers.tsx

@@ -0,0 +1,352 @@
+import React, { useState } from 'react';
+import {
+  Button, Table, Space, Form, Input, Select, DatePicker,
+  message, Modal, Card, Typography, Tag, Popconfirm
+} from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { believerClient } 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'];
+
+const { Title } = Typography;
+const { Option } = Select;
+
+// 信徒管理页面
+export const BelieversPage = () => {
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    pageSize: 10,
+    keyword: ''
+  });
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalTitle, setModalTitle] = useState('');
+  const [editingBeliever, setEditingBeliever] = useState<any>(null);
+  const [form] = Form.useForm();
+
+  const { data: believersData, isLoading, refetch } = useQuery({
+    queryKey: ['believers', searchParams],
+    queryFn: async () => {
+      const res = await believerClient.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.pageSize,
+          keyword: searchParams.keyword
+        }
+      });
+      if (res.status !== 200) {
+        throw new Error('获取信徒列表失败');
+      }
+      return await res.json();
+    }
+  });
+
+  const believers = believersData?.data || [];
+  const pagination = {
+    current: searchParams.page,
+    pageSize: searchParams.pageSize,
+    total: believersData?.pagination?.total || 0
+  };
+
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams(prev => ({
+      ...prev,
+      search: values.search || '',
+      page: 1
+    }));
+  };
+
+  // 处理分页变化
+  const handleTableChange = (newPagination: any) => {
+    setSearchParams(prev => ({
+      ...prev,
+      page: newPagination.current,
+      pageSize: newPagination.pageSize
+    }));
+  };
+
+  // 打开创建信徒模态框
+  const showCreateModal = () => {
+    setModalTitle('创建信徒');
+    setEditingBeliever(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 打开编辑信徒模态框
+  const showEditModal = (believer: any) => {
+    setModalTitle('编辑信徒');
+    setEditingBeliever(believer);
+    form.setFieldsValue({
+      ...believer,
+      birthDate: believer.birthDate ? dayjs(believer.birthDate) : null,
+      baptismDate: believer.baptismDate ? dayjs(believer.baptismDate) : null
+    });
+    setModalVisible(true);
+  };
+
+  // 处理模态框确认
+  const handleModalOk = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      // 转换日期格式
+      const formattedValues = {
+        ...values,
+        birthDate: values.birthDate ? values.birthDate.format('YYYY-MM-DD') : null,
+        baptismDate: values.baptismDate ? values.baptismDate.format('YYYY-MM-DD') : null
+      };
+      
+      if (editingBeliever) {
+        // 编辑信徒
+        const res = await believerClient[':id']['$put']({
+          param: { id: editingBeliever.id },
+          json: formattedValues
+        });
+        if (res.status !== 200) {
+          throw new Error('更新信徒失败');
+        }
+        message.success('信徒更新成功');
+      } else {
+        // 创建信徒
+        const res = await believerClient.$post({
+          json: formattedValues
+        });
+        if (res.status !== 201) {
+          throw new Error('创建信徒失败');
+        }
+        message.success('信徒创建成功');
+      }
+      
+      setModalVisible(false);
+      form.resetFields();
+      refetch(); // 刷新信徒列表
+    } catch (error) {
+      console.error('表单提交失败:', error);
+      message.error('操作失败,请重试');
+    }
+  };
+
+  // 处理删除信徒
+  const handleDelete = async (id: number) => {
+    try {
+      const res = await believerClient[':id']['$delete']({
+        param: { id }
+      });
+      if (res.status !== 204) {
+        throw new Error('删除信徒失败');
+      }
+      message.success('信徒删除成功');
+      refetch(); // 刷新信徒列表
+    } catch (error) {
+      console.error('删除信徒失败:', error);
+      message.error('删除失败,请重试');
+    }
+  };
+  
+  // 状态标签格式化
+  const formatStatusTag = (status: string) => {
+    const statusMap: Record<string, { label: string; color: string }> = {
+      [BelieverStatus.ACTIVE]: { label: '活跃', color: 'green' },
+      [BelieverStatus.INACTIVE]: { label: '不活跃', color: 'orange' },
+      [BelieverStatus.TRANSFERRED]: { label: '已转会', color: 'purple' },
+      [BelieverStatus.DECEASED]: { label: '已故', color: 'red' }
+    };
+    
+    const statusInfo = statusMap[status] || { label: status, color: 'default' };
+    return <Tag color={statusInfo.color}>{statusInfo.label}</Tag>;
+  };
+  
+  const columns = [
+    {
+      title: '姓名',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '性别',
+      dataIndex: 'gender',
+      key: 'gender',
+    },
+    {
+      title: '出生日期',
+      dataIndex: 'birthDate',
+      key: 'birthDate',
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-',
+    },
+    {
+      title: '联系方式',
+      dataIndex: 'contactInfo',
+      key: 'contactInfo',
+    },
+    {
+      title: '受洗日期',
+      dataIndex: 'baptismDate',
+      key: 'baptismDate',
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: string) => formatStatusTag(status),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: any) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => showEditModal(record)}>
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定要删除此信徒记录吗?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger>
+              删除
+            </Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div>
+      <Title level={2}>信徒管理</Title>
+      <Card>
+        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+          <Form.Item name="search" label="搜索">
+            <Input placeholder="姓名/联系方式" allowClear />
+          </Form.Item>
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">
+                搜索
+              </Button>
+              <Button type="primary" onClick={showCreateModal}>
+                创建信徒
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+
+        <Table
+          columns={columns}
+          dataSource={believers}
+          loading={isLoading}
+          rowKey="id"
+          pagination={{
+            ...pagination,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total) => `共 ${total} 条记录`
+          }}
+          onChange={handleTableChange}
+        />
+      </Card>
+
+      {/* 创建/编辑信徒模态框 */}
+      <Modal
+        title={modalTitle}
+        open={modalVisible}
+        onOk={handleModalOk}
+        onCancel={() => {
+          setModalVisible(false);
+          form.resetFields();
+        }}
+        width={600}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+        >
+          <Form.Item
+            name="name"
+            label="姓名"
+            rules={[
+              { required: true, message: '请输入姓名' },
+              { max: 100, message: '姓名不能超过100个字符' }
+            ]}
+          >
+            <Input placeholder="请输入姓名" />
+          </Form.Item>
+
+          <Form.Item
+            name="gender"
+            label="性别"
+            rules={[{ required: true, message: '请选择性别' }]}
+          >
+            <Select placeholder="请选择性别">
+              <Option value="男">男</Option>
+              <Option value="女">女</Option>
+            </Select>
+          </Form.Item>
+
+          <Form.Item
+            name="birthDate"
+            label="出生日期"
+          >
+            <DatePicker format="YYYY-MM-DD" placeholder="请选择出生日期" />
+          </Form.Item>
+
+          <Form.Item
+            name="contactInfo"
+            label="联系方式"
+            rules={[
+              { required: true, message: '请输入联系方式' },
+              { max: 200, message: '联系方式不能超过200个字符' }
+            ]}
+          >
+            <Input placeholder="请输入联系方式" />
+          </Form.Item>
+
+          <Form.Item
+            name="address"
+            label="住址"
+            rules={[
+              { max: 255, message: '住址不能超过255个字符' }
+            ]}
+          >
+            <Input placeholder="请输入住址" />
+          </Form.Item>
+
+          <Form.Item
+            name="baptismDate"
+            label="受洗日期"
+          >
+            <DatePicker format="YYYY-MM-DD" placeholder="请选择受洗日期" />
+          </Form.Item>
+
+          <Form.Item
+            name="status"
+            label="状态"
+            rules={[{ required: true, message: '请选择状态' }]}
+          >
+            <Select placeholder="请选择状态">
+              <Option value={BelieverStatus.ACTIVE}>活跃</Option>
+              <Option value={BelieverStatus.INACTIVE}>不活跃</Option>
+              <Option value={BelieverStatus.TRANSFERRED}>已转会</Option>
+              <Option value={BelieverStatus.DECEASED}>已故</Option>
+            </Select>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

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

@@ -6,6 +6,7 @@ import { ErrorPage } from './components/ErrorPage';
 import { NotFoundPage } from './components/NotFoundPage';
 import { DashboardPage } from './pages/Dashboard';
 import { UsersPage } from './pages/Users';
+import { BelieversPage } from './pages/Believers';
 import { LoginPage } from './pages/Login';
 
 export const router = createBrowserRouter([
@@ -39,6 +40,11 @@ export const router = createBrowserRouter([
         element: <UsersPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'believers',
+        element: <BelieversPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 5 - 1
src/client/api.ts

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

+ 68 - 0
src/server/api/believers/[id]/delete.ts

@@ -0,0 +1,68 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { BelieverService } from '@/server/modules/believers/believer.service';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { AuthContext } from '@/server/types/context';
+
+const GetParams = z.object({
+  id: z.string().openapi({
+    param: { name: 'id', in: 'path' },
+    example: '1',
+    description: '信徒ID'
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: GetParams
+  },
+  responses: {
+    204: {
+      description: '成功删除信徒记录'
+    },
+    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 believerService = new BelieverService(AppDataSource);
+    
+    const deleted = await believerService.deleteBeliever(parseInt(id));
+    if (!deleted) {
+      return c.json({ code: 404, message: '信徒不存在' }, 404);
+    }
+    
+    return c.body(null, 204);
+  } catch (error) {
+    return c.json({ 
+      code: 500, 
+      message: error instanceof Error ? error.message : '删除信徒记录失败' 
+    }, 500);
+  }
+});
+
+export default app;

+ 59 - 0
src/server/api/believers/[id]/get.ts

@@ -0,0 +1,59 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { BelieverService } from '@/server/modules/believers/believer.service';
+import { z } from '@hono/zod-openapi';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AuthContext } from '@/server/types/context';
+import { BelieverSchema } from '@/server/modules/believers/believer.entity';
+
+const believerService = new BelieverService(AppDataSource);
+
+const GetParams = z.object({
+  id: z.string().openapi({
+    param: { name: 'id', in: 'path' },
+    example: '1',
+    description: '信徒ID'
+  })
+});
+
+const routeDef = createRoute({
+  method: 'get',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: GetParams
+  },
+  responses: {
+    200: {
+      description: '成功获取信徒详情',
+      content: { 'application/json': { schema: BelieverSchema } }
+    },
+    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 believer = await believerService.getBelieverById(parseInt(id));
+    if (!believer) {
+      return c.json({ code: 404, message: '信徒不存在' }, 404);
+    }
+    return c.json(believer, 200);
+  } catch (error) {
+    return c.json({ 
+      code: 500, 
+      message: error instanceof Error ? error.message : '获取信徒详情失败' 
+    }, 500);
+  }
+});
+
+export default app;

+ 102 - 0
src/server/api/believers/[id]/put.ts

@@ -0,0 +1,102 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { BelieverService } from '@/server/modules/believers/believer.service';
+import { Believer, BelieverSchema, BelieverStatus } from '@/server/modules/believers/believer.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { AuthContext } from '@/server/types/context';
+
+// 更新信徒请求Schema
+const UpdateBelieverSchema = BelieverSchema.omit({
+  id: true,
+  createdAt: true,
+  updatedAt: true,
+  createdBy: true
+});
+
+const GetParams = z.object({
+  id: z.string().openapi({
+    param: { name: 'id', in: 'path' },
+    example: '1',
+    description: '信徒ID'
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: GetParams,
+    body: {
+      content: {
+        'application/json': {
+          schema: UpdateBelieverSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功更新信徒记录',
+      content: {
+        'application/json': {
+          schema: BelieverSchema
+        }
+      }
+    },
+    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 data = c.req.valid('json');
+    
+    const believerService = new BelieverService(AppDataSource);
+    const believer = await believerService.updateBeliever(parseInt(id), data);
+    
+    if (!believer) {
+      return c.json({ code: 404, message: '信徒不存在' }, 404);
+    }
+    
+    return c.json(believer, 200);
+  } catch (error) {
+    if (error instanceof z.ZodError) {
+      return c.json({ code: 400, message: '参数错误' }, 400);
+    }
+    return c.json({ 
+      code: 500, 
+      message: error instanceof Error ? error.message : '更新信徒记录失败' 
+    }, 500);
+  }
+});
+
+export default app;

+ 7 - 1
src/server/api/believers/index.ts

@@ -1,9 +1,15 @@
 import { OpenAPIHono } from '@hono/zod-openapi';
 import listBelieversRoute from './get';
 import createBelieverRoute from './post';
+import getBelieverByIdRoute from './[id]/get';
+import updateBelieverRoute from './[id]/put';
+import deleteBelieverRoute from './[id]/delete';
 
 const app = new OpenAPIHono()
   .route('/', listBelieversRoute)
-  .route('/', createBelieverRoute);
+  .route('/', createBelieverRoute)
+  .route('/', getBelieverByIdRoute)
+  .route('/', updateBelieverRoute)
+  .route('/', deleteBelieverRoute);
 
 export default app;