Kaynağa Gözat

邮件模板和邮件提醒先写了 等后期在写

pm 5 ay önce
ebeveyn
işleme
ee450b75f4

+ 2 - 1
src/Dashboard.tsx

@@ -10,6 +10,7 @@ import IsolationWork from './components/IsolationWork';
 import ProfileSettings from './components/ProfileSettings';
 import CockpitDashboard from './components/CockpitDashboard';
 import LockCabinetDetail from './components/lockCabinet/LockCabinetDetail';
+import NotificationManagement from './components/NotificationManagement';
 import { authApi } from './api';
 import { toast } from 'sonner';
 import { Toaster } from 'sonner';
@@ -920,7 +921,7 @@ export default function Dashboard() {
         ) : activeMenu === 'isolationWork' ? (
           <IsolationWork subMenu={activeSubMenu} />
         ) : activeMenu === 'notificationManagement' ? (
-          <UserManagement subMenu={activeSubMenu} />
+          <NotificationManagement />
         ) : (
           // 无法映射的菜单(如客户端菜单)显示占位内容
           <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-8">

+ 115 - 0
src/api/emailTemplate/index.ts

@@ -0,0 +1,115 @@
+import { request } from '../../utils/axios';
+
+// 邮件模板 VO 类型
+export interface MailTemplateVO {
+  id?: number;
+  name: string;
+  code: string;
+  accountId?: number;
+  nickname?: string;
+  title?: string;
+  content?: string;
+  params?: string;
+  status?: number;
+  remark?: string;
+}
+
+// 邮件发送请求 VO 类型
+export interface MailSendReqVO {
+  mail: string;
+  templateCode: string;
+  templateParams: Record<string, any>;
+}
+
+// 分页参数类型
+export interface PageParam {
+  pageNo?: number;
+  pageSize?: number;
+  name?: string;
+  code?: string;
+  status?: number;
+  [key: string]: any;
+}
+
+// 分页响应类型
+export interface PageResponse<T> {
+  list: T[];
+  total: number;
+  records?: T[]; // 兼容其他响应格式
+  data?: {
+    list: T[];
+    total: number;
+  };
+}
+
+// 邮件模板 API
+export const emailTemplateApi = {
+  // 查询邮件模版列表
+  getMailTemplatePage: async (params: PageParam): Promise<PageResponse<MailTemplateVO>> => {
+    const response: any = await request.get({
+      url: '/system/mail-template/page',
+      params,
+    });
+    
+    // 处理响应数据格式
+    if (response && typeof response === 'object') {
+      if ('data' in response && response.data) {
+        return response.data;
+      } else if ('list' in response || 'records' in response) {
+        return {
+          list: response.list || response.records || [],
+          total: response.total || 0,
+        };
+      }
+    }
+    return { list: [], total: 0 };
+  },
+
+  // 查询邮件模版详情
+  getMailTemplate: async (id: number): Promise<MailTemplateVO> => {
+    const response = await request.get({
+      url: `/system/mail-template/get?id=${id}`,
+    });
+    return response?.data || response;
+  },
+
+  // 新增邮件模版
+  createMailTemplate: async (data: MailTemplateVO): Promise<void> => {
+    await request.post({
+      url: '/system/mail-template/create',
+      data,
+    });
+  },
+
+  // 修改邮件模版
+  updateMailTemplate: async (data: MailTemplateVO): Promise<void> => {
+    await request.put({
+      url: '/system/mail-template/update',
+      data,
+    });
+  },
+
+  // 删除邮件模版
+  deleteMailTemplate: async (id: number): Promise<void> => {
+    await request.delete({
+      url: `/system/mail-template/delete?id=${id}`,
+    });
+  },
+
+  // 发送邮件
+  sendMail: async (data: MailSendReqVO): Promise<string | number | null> => {
+    const response: any = await request.post({
+      url: '/system/mail-template/send-mail',
+      data,
+    });
+    return response?.data || response || null;
+  },
+};
+
+// 为了兼容旧代码,导出别名
+export const listEmailTemplates = emailTemplateApi.getMailTemplatePage;
+export const getEmailTemplatesInfo = emailTemplateApi.getMailTemplate;
+export const addEmailTemplates = emailTemplateApi.createMailTemplate;
+export const updateEmailTemplates = emailTemplateApi.updateMailTemplate;
+export const delEmailTemplates = emailTemplateApi.deleteMailTemplate;
+

+ 6 - 0
src/api/index.ts

@@ -18,6 +18,8 @@ import { padLockTypeApi } from './PadLockType';
 import { lockCabinetApi } from './lockCabinet';
 import { workstationApi } from './workstation';
 import { fileApi } from './file';
+import { mailNotifyConfigApi } from './mailNotifyConfig';
+import { emailTemplateApi } from './emailTemplate';
 
 // API 响应类型
 export interface ApiResponse<T = any> {
@@ -47,6 +49,8 @@ export { padLockTypeApi } from './PadLockType';
 export { lockCabinetApi } from './lockCabinet';
 export { workstationApi } from './workstation';
 export { fileApi } from './file';
+export { mailNotifyConfigApi } from './mailNotifyConfig';
+export { emailTemplateApi } from './emailTemplate';
 
 // 为了兼容旧代码,导出 authApi 作为 loginApi 的别名
 export { loginApi as authApi } from './Login';
@@ -75,5 +79,7 @@ export default {
   lockCabinet: lockCabinetApi,
   workstation: workstationApi,
   file: fileApi,
+  mailNotifyConfig: mailNotifyConfigApi,
+  emailTemplate: emailTemplateApi,
 };
 

+ 97 - 0
src/api/mailNotifyConfig/index.ts

@@ -0,0 +1,97 @@
+import { request } from '../../utils/axios';
+
+// 邮件提醒周期配置 VO 类型
+export interface MailNotifyConfigVO {
+  configId?: number;
+  configName?: string;
+  configCode?: string;
+  name?: string; // 提醒事项
+  templateCode?: string; // 邮件模板编码
+  reminderTime?: number; // 提醒时长(秒)
+  notifyCycle?: string;
+  notifyTime?: string;
+  notifyType?: string;
+  status?: string;
+  remark?: string;
+  createTime?: Date;
+}
+
+// 分页参数类型
+export interface PageParam {
+  pageNo: number;
+  pageSize: number;
+  configName?: string;
+  configCode?: string;
+  name?: string; // 提醒事项
+  templateName?: string; // 邮件模板名称
+  status?: string;
+}
+
+// 分页响应类型
+export interface PageResponse<T> {
+  list: T[];
+  total: number;
+  records?: T[]; // 兼容其他响应格式
+  data?: {
+    list: T[];
+    total: number;
+  };
+}
+
+// 邮件提醒周期配置 API
+export const mailNotifyConfigApi = {
+  // 查看系统邮件提醒周期配置-分页
+  listIsMailNotifyConfigPage: async (params: PageParam): Promise<PageResponse<MailNotifyConfigVO>> => {
+    const response: any = await request.get({
+      url: '/iscs/mail-notify-config/getMailNotifyConfigPage',
+      params,
+    });
+    
+    // 处理响应数据格式
+    if (response && typeof response === 'object') {
+      if ('data' in response && response.data) {
+        return response.data;
+      } else if ('list' in response || 'records' in response) {
+        return {
+          list: response.list || response.records || [],
+          total: response.total || 0,
+        };
+      }
+    }
+    return { list: [], total: 0 };
+  },
+
+  // 获取系统邮件提醒周期配置详细信息
+  getIsMailNotifyConfigById: async (id: number): Promise<MailNotifyConfigVO> => {
+    const response = await request.get({
+      url: '/iscs/mail-notify-config/selectMailNotifyConfigById',
+      params: { id },
+    });
+    return response?.data || response;
+  },
+
+  // 新增系统邮件提醒周期配置
+  addIsMailNotifyConfig: async (data: MailNotifyConfigVO): Promise<void> => {
+    await request.post({
+      url: '/iscs/mail-notify-config/insertMailNotifyConfig',
+      data,
+    });
+  },
+
+  // 修改系统邮件提醒周期配置
+  updateIsMailNotifyConfig: async (data: MailNotifyConfigVO): Promise<void> => {
+    await request.put({
+      url: '/iscs/mail-notify-config/updateMailNotifyConfig',
+      data,
+    });
+  },
+
+  // 删除系统邮件提醒周期配置
+  deleteIsMailNotifyConfig: async (ids: number | number[]): Promise<void> => {
+    const idStr = Array.isArray(ids) ? ids.join(',') : String(ids);
+    await request.delete({
+      url: `/iscs/mail-notify-config/deleteMailNotifyConfigList?ids=${idStr}`,
+    });
+  },
+};
+

+ 84 - 74
src/components/NotificationManagement.tsx

@@ -1,7 +1,8 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
 import { Plus, Search, Edit2, Trash2, MoreVertical, Mail, Phone, MessageSquare, Smartphone } from 'lucide-react';
 import { Button } from 'antd';
 import { Button as UIButton } from './ui/button';
+import EmailNotifyManagement from './notification/EmailNotifyManagement';
 
 interface TableRow {
   id: number;
@@ -14,6 +15,11 @@ export default function NotificationManagement() {
   const [editingItem, setEditingItem] = useState<TableRow | null>(null);
   const [notificationType, setNotificationType] = useState<string>('email'); // 通知类型:email, sms, message, app
 
+  // 调试日志
+  useEffect(() => {
+    console.log('NotificationManagement 组件已加载,notificationType:', notificationType);
+  }, [notificationType]);
+
   // 通知管理数据
   const notificationData: TableRow[] = [
     {
@@ -252,88 +258,92 @@ export default function NotificationManagement() {
 
   return (
     <div className="space-y-6">
-      {/* Tab按钮栏 */}
-      <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-2">
-        <div className="flex items-center gap-2">
-          <button
-            onClick={() => setNotificationType('email')}
-            className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
-              notificationType === 'email'
-                ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
-                : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
-            }`}
-          >
-            <Mail className="w-4 h-4" />
-            <span>邮件</span>
-          </button>
-          <button
-            onClick={() => setNotificationType('sms')}
-            className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
-              notificationType === 'sms'
-                ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
-                : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
-            }`}
-          >
-            <Phone className="w-4 h-4" />
-            <span>短信</span>
-          </button>
-          <button
-            onClick={() => setNotificationType('message')}
-            className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
-              notificationType === 'message'
-                ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
-                : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
-            }`}
-          >
-            <MessageSquare className="w-4 h-4" />
-            <span>站内信</span>
-          </button>
-          <button
-            onClick={() => setNotificationType('app')}
-            className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
-              notificationType === 'app'
-                ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
-                : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
-            }`}
-          >
-            <Smartphone className="w-4 h-4" />
-            <span>APP通知</span>
-          </button>
+      {/* Tab按钮栏 - 只在非邮件类型时显示 */}
+      {notificationType !== 'email' && (
+        <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-2">
+          <div className="flex items-center gap-2">
+            <button
+              onClick={() => setNotificationType('email')}
+              className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
+                notificationType === 'email'
+                  ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
+                  : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
+              }`}
+            >
+              <Mail className="w-4 h-4" />
+              <span>邮件</span>
+            </button>
+            <button
+              onClick={() => setNotificationType('sms')}
+              className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
+                notificationType === 'sms'
+                  ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
+                  : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
+              }`}
+            >
+              <Phone className="w-4 h-4" />
+              <span>短信</span>
+            </button>
+            <button
+              onClick={() => setNotificationType('message')}
+              className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
+                notificationType === 'message'
+                  ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
+                  : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
+              }`}
+            >
+              <MessageSquare className="w-4 h-4" />
+              <span>站内信</span>
+            </button>
+            <button
+              onClick={() => setNotificationType('app')}
+              className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
+                notificationType === 'app'
+                  ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
+                  : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
+              }`}
+            >
+              <Smartphone className="w-4 h-4" />
+              <span>APP通知</span>
+            </button>
+          </div>
         </div>
-      </div>
+      )}
 
       {/* 表格容器 */}
       <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
-        {/* 工具栏 */}
-        <div className="p-4 border-b border-gray-200/50">
-          <div className="flex items-center justify-between">
-            <div className="relative w-80">
-              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
-              <input
-                type="text"
-                placeholder="搜索通知..."
-                value={searchTerm}
-                onChange={(e) => setSearchTerm(e.target.value)}
-                className="w-full h-10 pl-10 pr-4 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-              />
-            </div>
+        {/* 工具栏 - 只在非邮件类型时显示 */}
+        {notificationType !== 'email' && (
+          <div className="p-4 border-b border-gray-200/50">
+            <div className="flex items-center justify-between">
+              <div className="relative w-80">
+                <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
+                <input
+                  type="text"
+                  placeholder="搜索通知..."
+                  value={searchTerm}
+                  onChange={(e) => setSearchTerm(e.target.value)}
+                  className="w-full h-10 pl-10 pr-4 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
+                />
+              </div>
 
-            <button
-              onClick={() => {
-                setEditingItem(null);
-                setShowAddModal(true);
-              }}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
-            >
-              <Plus className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">新增通知</span>
-            </button>
+              <button
+                onClick={() => {
+                  setEditingItem(null);
+                  setShowAddModal(true);
+                }}
+                className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
+              >
+                <Plus className="w-4 h-4" strokeWidth={2.5} />
+                <span className="text-sm">新增通知</span>
+              </button>
+            </div>
           </div>
-        </div>
+        )}
 
         {/* 通知内容区域 */}
         <div>
-          {notificationType === 'email' && renderTable()}
+          {notificationType === 'email' && <EmailNotifyManagement />}
           {notificationType === 'sms' && renderTable()}
           {notificationType === 'message' && renderTable()}
           {notificationType === 'app' && renderTable()}

+ 4 - 14
src/components/SegregationPointForm.tsx

@@ -5,7 +5,6 @@ import type { UploadFile } from 'antd/es/upload/interface';
 import { segregationPointApi, SegregationPointVO } from '../api/spm/index';
 import { marsDeptApi } from '../api/marsdept/index';
 import { lotoStationApi } from '../api/lotoStation/index';
-import { rfidApi } from '../api/rfid/index';
 import { loginApi } from '../api/Login';
 import { handleTree } from '../utils/tree';
 import { getStrDictOptions, DICT_TYPE } from '../utils/dict';
@@ -32,7 +31,6 @@ const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPoin
     // 下拉选项数据
     const [deptOptions, setDeptOptions] = useState<any[]>([]);
     const [lotoOptions, setLotoOptions] = useState<Array<{ label: string; value: number }>>([]);
-    const [rfidTokenData, setRfidTokenData] = useState<Array<{ label: string; value: number }>>([]);
     const powerTypeOptions = getStrDictOptions(DICT_TYPE.POWER_TYPE);
 
     // 打开弹窗
@@ -58,7 +56,7 @@ const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPoin
             pointName: data.pointName,
             pointIcon: data.pointIcon,
             pointPicture: data.pointPicture,
-            rfidId: data.rfidId,
+            pointNfc: data.pointNfc,
             workstationId: data.workstationId,
             lotoId: data.lotoId,
             powerType: data.powerType,
@@ -93,13 +91,6 @@ const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPoin
           value: item.id!,
           label: item.lotoName,
         })));
-
-        // 获取RFID Token数据
-        const rfidRes = await rfidApi.getIsRfidTokenPage({ pageNo: 1, pageSize: -1 });
-        setRfidTokenData(rfidRes.list.map(record => ({
-          value: record.id!,
-          label: record.rfid,
-        })));
       } catch (error) {
         console.error('加载选项数据失败:', error);
       }
@@ -175,7 +166,7 @@ const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPoin
             pointName: values.pointName,
             pointIcon: values.pointIcon,
             pointPicture: values.pointPicture,
-            rfidId: values.rfidId,
+            pointNfc: values.pointNfc,
             workstationId: values.workstationId,
             lotoId: values.lotoId,
             powerType: values.powerType,
@@ -258,10 +249,9 @@ const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPoin
             <Col span={12}>
               <Form.Item
                 label="隔离点NFC"
-                name="rfidId"
-                rules={[{ required: true, message: '隔离点NFC不能为空' }]}
+                name="pointNfc"
               >
-                <Select placeholder="请选择隔离点NFC" options={rfidTokenData} />
+                <Input placeholder="请输入隔离点NFC" />
               </Form.Item>
             </Col>
           </Row>

+ 192 - 0
src/components/mailTemplate/MailTemplateForm.tsx

@@ -0,0 +1,192 @@
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { Modal, Form, Input, InputNumber, Select, Row, Col, message } from 'antd';
+import { emailTemplateApi, MailTemplateVO } from '../../api/emailTemplate';
+
+const { TextArea } = Input;
+
+interface MailTemplateFormProps {
+  onSuccess?: () => void;
+}
+
+export interface MailTemplateFormRef {
+  open: (type: string, id?: number) => void;
+}
+
+const MailTemplateForm = forwardRef<MailTemplateFormRef, MailTemplateFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [dialogTitle, setDialogTitle] = useState('');
+  const [formLoading, setFormLoading] = useState(false);
+  const [formType, setFormType] = useState<'create' | 'update'>('create');
+  const [form] = Form.useForm();
+
+  // 打开弹窗
+  const open = async (type: string, id?: number) => {
+    setDialogVisible(true);
+    setDialogTitle(type === 'create' ? '新增邮件模板' : '编辑邮件模板');
+    setFormType(type as 'create' | 'update');
+    form.resetFields();
+
+    // 修改时,设置数据
+    if (id) {
+      setFormLoading(true);
+      try {
+        const data = await emailTemplateApi.getMailTemplate(id);
+        form.setFieldsValue({
+          id: data.id,
+          name: data.name,
+          code: data.code,
+          accountId: data.accountId,
+          nickname: data.nickname,
+          title: data.title,
+          content: data.content,
+          params: data.params,
+          status: data.status,
+          remark: data.remark,
+        });
+      } catch (error: any) {
+        message.error(error.message || '获取邮件模板详情失败');
+      } finally {
+        setFormLoading(false);
+      }
+    } else {
+      // 新增时设置默认值
+      form.setFieldsValue({
+        status: 1,
+      });
+    }
+  };
+
+  useImperativeHandle(ref, () => ({
+    open,
+  }));
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      const values = await form.validateFields();
+
+      setFormLoading(true);
+      try {
+        const data: MailTemplateVO = {
+          ...values,
+        };
+
+        if (formType === 'create') {
+          await emailTemplateApi.createMailTemplate(data);
+          message.success('创建成功');
+        } else {
+          await emailTemplateApi.updateMailTemplate(data);
+          message.success('更新成功');
+        }
+        setDialogVisible(false);
+        onSuccess?.();
+      } catch (error: any) {
+        message.error(error.message || '操作失败');
+      } finally {
+        setFormLoading(false);
+      }
+    } catch (error) {
+      // 表单验证失败
+    }
+  };
+
+  return (
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={800}
+      destroyOnClose
+    >
+      <Form
+        form={form}
+        labelCol={{ span: 6 }}
+        wrapperCol={{ span: 18 }}
+        initialValues={{
+          status: 1,
+        }}
+      >
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item
+              label="模板名称"
+              name="name"
+              rules={[{ required: true, message: '模板名称不能为空' }]}
+            >
+              <Input placeholder="请输入模板名称" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="模板编码"
+              name="code"
+              rules={[{ required: true, message: '模板编码不能为空' }]}
+            >
+              <Input placeholder="请输入模板编码" />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item label="账号ID" name="accountId">
+              <InputNumber placeholder="请输入账号ID" style={{ width: '100%' }} />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="昵称" name="nickname">
+              <Input placeholder="请输入昵称" />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item label="标题" name="title">
+              <Input placeholder="请输入标题" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="状态" name="status">
+              <Select placeholder="请选择状态">
+                <Select.Option value={1}>启用</Select.Option>
+                <Select.Option value={0}>禁用</Select.Option>
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Row gutter={16}>
+          <Col span={24}>
+            <Form.Item label="模板内容" name="content">
+              <TextArea rows={6} placeholder="请输入模板内容" />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Row gutter={16}>
+          <Col span={24}>
+            <Form.Item label="参数" name="params">
+              <Input placeholder="请输入参数,多个参数用逗号分隔,如:name,code" />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Row gutter={16}>
+          <Col span={24}>
+            <Form.Item label="备注" name="remark">
+              <TextArea rows={3} placeholder="请输入备注" />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Modal>
+  );
+});
+
+MailTemplateForm.displayName = 'MailTemplateForm';
+
+export default MailTemplateForm;
+

+ 295 - 0
src/components/mailTemplate/MailTemplateManagement.tsx

@@ -0,0 +1,295 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Search, Plus, RefreshCw, ArrowLeft, Edit2, Trash2 } from 'lucide-react';
+import { emailTemplateApi, MailTemplateVO } from '../../api/emailTemplate';
+import { toast } from 'sonner';
+import { Modal, Table, Input, Button, Space, Switch } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { Button as UIButton } from '../ui/button';
+import type { ColumnsType } from 'antd/es/table';
+import MailTemplateForm, { MailTemplateFormRef } from './MailTemplateForm';
+import MailTemplateSendForm, { MailTemplateSendFormRef } from './MailTemplateSendForm';
+
+interface MailTemplateManagementProps {
+  onBack?: () => void;
+}
+
+export default function MailTemplateManagement(props: MailTemplateManagementProps = {}) {
+  const { onBack } = props;
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<MailTemplateVO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState({
+    pageNo: 1,
+    pageSize: 10,
+    name: '',
+    code: '',
+    status: undefined as number | undefined,
+  });
+
+  // 子组件引用
+  const formRef = useRef<MailTemplateFormRef>(null);
+  const sendFormRef = useRef<MailTemplateSendFormRef>(null);
+
+  // 获取列表
+  const getList = async (params?: typeof queryParams) => {
+    const currentParams = params || queryParams;
+    setLoading(true);
+    try {
+      const response = await emailTemplateApi.getMailTemplatePage(currentParams);
+      setList(response.list || []);
+      setTotal(response.total || 0);
+    } catch (error: any) {
+      toast.error(error.message || '获取邮件模板列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    getList();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [queryParams.pageNo, queryParams.pageSize]);
+
+  // 搜索
+  const handleQuery = () => {
+    const newParams = { ...queryParams, pageNo: 1 };
+    setQueryParams(newParams);
+    getList(newParams);
+  };
+
+  // 重置搜索
+  const resetQuery = () => {
+    const resetParams = {
+      pageNo: 1,
+      pageSize: 10,
+      name: '',
+      code: '',
+      status: undefined,
+    };
+    setQueryParams(resetParams);
+    getList(resetParams);
+  };
+
+  // 打开表单弹窗
+  const openForm = (type: string, id?: number) => {
+    formRef.current?.open(type, id);
+  };
+
+  // 打开发送测试弹窗
+  const openSendForm = (id: number) => {
+    sendFormRef.current?.open(id);
+  };
+
+  // 删除
+  const handleDelete = (id: number) => {
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: '确定要删除这条邮件模板吗?',
+      okText: '确定',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await emailTemplateApi.deleteMailTemplate(id);
+          toast.success('删除成功');
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 表格列配置
+  const columns: ColumnsType<MailTemplateVO> = [
+    {
+      title: '序号',
+      width: 80,
+      align: 'center',
+      render: (_: any, __: MailTemplateVO, index: number) => {
+        return (queryParams.pageNo - 1) * queryParams.pageSize + index + 1;
+      },
+    },
+    {
+      title: '模板名称',
+      dataIndex: 'name',
+      key: 'name',
+      align: 'center',
+    },
+    {
+      title: '模板编码',
+      dataIndex: 'code',
+      key: 'code',
+      align: 'center',
+    },
+    {
+      title: '标题',
+      dataIndex: 'title',
+      key: 'title',
+      align: 'center',
+      render: (text: string) => text || '-',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      width: 100,
+      align: 'center',
+      render: (status: number | undefined) => {
+        return status === 1 ? '启用' : '禁用';
+      },
+    },
+    {
+      title: '备注',
+      dataIndex: 'remark',
+      key: 'remark',
+      align: 'center',
+      render: (text: string) => text || '-',
+    },
+    {
+      title: '操作',
+      width: 200,
+      align: 'center',
+      fixed: 'right',
+      render: (_: any, record: MailTemplateVO) => {
+        return (
+          <div className="flex items-center gap-2 justify-center">
+            <UIButton
+              variant="ghost"
+              size="sm"
+              onClick={() => openSendForm(record.id!)}
+              className="h-8 px-2"
+            >
+              <Search className="w-4 h-4" />
+              <span className="ml-1">测试</span>
+            </UIButton>
+            <UIButton
+              variant="ghost"
+              size="sm"
+              onClick={() => openForm('update', record.id)}
+              className="h-8 px-2"
+            >
+              <Edit2 className="w-4 h-4" />
+              <span className="ml-1">编辑</span>
+            </UIButton>
+            <UIButton
+              variant="ghost"
+              size="sm"
+              onClick={() => handleDelete(record.id!)}
+              className="h-8 px-2 text-red-600 hover:text-red-700"
+            >
+              <Trash2 className="w-4 h-4" />
+              <span className="ml-1">删除</span>
+            </UIButton>
+          </div>
+        );
+      },
+    },
+  ];
+
+  return (
+    <div className="space-y-6">
+      {/* 搜索栏 */}
+      <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-6">
+        <div className="flex items-center gap-4 flex-wrap">
+          <div className="flex items-center gap-2 flex-1 min-w-[200px]">
+            <label className="text-sm text-gray-700 whitespace-nowrap">模板名称:</label>
+            <Input
+              placeholder="请输入模板名称"
+              value={queryParams.name}
+              onChange={(e) => setQueryParams({ ...queryParams, name: e.target.value })}
+              onPressEnter={handleQuery}
+              className="flex-1 max-w-[240px]"
+              allowClear
+            />
+          </div>
+          <div className="flex items-center gap-2 flex-1 min-w-[200px]">
+            <label className="text-sm text-gray-700 whitespace-nowrap">模板编码:</label>
+            <Input
+              placeholder="请输入模板编码"
+              value={queryParams.code}
+              onChange={(e) => setQueryParams({ ...queryParams, code: e.target.value })}
+              onPressEnter={handleQuery}
+              className="flex-1 max-w-[240px]"
+              allowClear
+            />
+          </div>
+          <div className="flex items-center gap-2">
+            <Button
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
+              onClick={handleQuery}
+            >
+              搜索
+            </Button>
+            <Button icon={<RefreshCw className="w-4 h-4" />} onClick={resetQuery}>
+              重置
+            </Button>
+            <Button
+              type="primary"
+              icon={<Plus className="w-4 h-4" />}
+              onClick={() => openForm('create')}
+            >
+              新增
+            </Button>
+            {onBack && (
+              <Button
+                icon={<ArrowLeft className="w-4 h-4" />}
+                onClick={onBack}
+              >
+                返回邮件提醒
+              </Button>
+            )}
+          </div>
+        </div>
+      </div>
+
+      {/* 表格 */}
+      <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
+        <Table
+          columns={columns}
+          dataSource={list}
+          rowKey="id"
+          loading={loading}
+          pagination={false}
+          scroll={{ x: 'max-content' }}
+        />
+        {/* 分页 */}
+        {!loading && list.length > 0 && (
+          <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
+            <div className="flex items-center justify-between">
+              <div className="text-sm text-gray-600">
+                共 <span className="text-blue-600 font-medium">{total}</span> 条记录
+              </div>
+              <div className="flex items-center gap-2">
+                <Button
+                  size="small"
+                  disabled={queryParams.pageNo === 1}
+                  onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
+                >
+                  上一页
+                </Button>
+                <span className="text-sm text-gray-600">
+                  第 {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize)} 页
+                </span>
+                <Button
+                  size="small"
+                  disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
+                  onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
+                >
+                  下一页
+                </Button>
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+
+      {/* 表单弹窗 */}
+      <MailTemplateForm ref={formRef} onSuccess={getList} />
+      {/* 发送测试弹窗 */}
+      <MailTemplateSendForm ref={sendFormRef} />
+    </div>
+  );
+}
+

+ 153 - 0
src/components/mailTemplate/MailTemplateSendForm.tsx

@@ -0,0 +1,153 @@
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { Modal, Form, Input, message } from 'antd';
+import { emailTemplateApi, MailSendReqVO } from '../../api/emailTemplate';
+
+const { TextArea } = Input;
+
+interface MailTemplateSendFormProps {
+  onSuccess?: () => void;
+}
+
+export interface MailTemplateSendFormRef {
+  open: (id: number) => void;
+}
+
+const MailTemplateSendForm = forwardRef<MailTemplateSendFormRef, MailTemplateSendFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [formLoading, setFormLoading] = useState(false);
+  const [form] = Form.useForm();
+  const [templateContent, setTemplateContent] = useState('');
+  const [templateParams, setTemplateParams] = useState<string[]>([]);
+  const [templateCode, setTemplateCode] = useState('');
+
+  // 打开弹窗
+  const open = async (id: number) => {
+    setDialogVisible(true);
+    form.resetFields();
+    setTemplateContent('');
+    setTemplateParams([]);
+    setTemplateCode('');
+
+    setFormLoading(true);
+    try {
+      const data = await emailTemplateApi.getMailTemplate(id);
+      setTemplateContent(data.content || '');
+      setTemplateCode(data.code || '');
+      
+      // 解析参数
+      if (data.params) {
+        const paramsArray = data.params.split(',').map((p: string) => p.trim()).filter((p: string) => p);
+        setTemplateParams(paramsArray);
+        
+        // 初始化表单参数值
+        const initialValues: Record<string, string> = {
+          mail: '',
+          templateCode: data.code,
+        };
+        paramsArray.forEach((param: string) => {
+          initialValues[`templateParams.${param}`] = '';
+        });
+        form.setFieldsValue(initialValues);
+      }
+    } catch (error: any) {
+      message.error(error.message || '获取邮件模板详情失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  useImperativeHandle(ref, () => ({
+    open,
+  }));
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      const values = await form.validateFields();
+
+      setFormLoading(true);
+      try {
+        // 构建模板参数对象
+        const params: Record<string, any> = {};
+        templateParams.forEach((param) => {
+          const value = values[`templateParams.${param}`];
+          if (value !== undefined && value !== null) {
+            params[param] = value;
+          }
+        });
+
+        const data: MailSendReqVO = {
+          mail: values.mail,
+          templateCode: templateCode,
+          templateParams: params,
+        };
+
+        const logId = await emailTemplateApi.sendMail(data);
+        if (logId) {
+          message.success(`提交发送成功!发送结果,见发送日志编号:${logId}`);
+        } else {
+          message.success('提交发送成功!');
+        }
+        setDialogVisible(false);
+        onSuccess?.();
+      } catch (error: any) {
+        message.error(error.message || '发送失败');
+      } finally {
+        setFormLoading(false);
+      }
+    } catch (error) {
+      // 表单验证失败
+    }
+  };
+
+  return (
+    <Modal
+      title="测试"
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={600}
+      destroyOnClose
+    >
+      <Form
+        form={form}
+        labelCol={{ span: 6 }}
+        wrapperCol={{ span: 18 }}
+      >
+        <Form.Item label="模板内容">
+          <TextArea
+            value={templateContent}
+            rows={6}
+            readOnly
+            style={{ backgroundColor: '#f5f5f5' }}
+          />
+        </Form.Item>
+
+        <Form.Item
+          label="收件邮箱"
+          name="mail"
+          rules={[{ required: true, message: '邮箱不能为空' }, { type: 'email', message: '请输入正确的邮箱地址' }]}
+        >
+          <Input placeholder="请输入收件邮箱" />
+        </Form.Item>
+
+        {templateParams.map((param) => (
+          <Form.Item
+            key={param}
+            label={`参数 {${param}}`}
+            name={`templateParams.${param}`}
+            rules={[{ required: true, message: `参数 ${param} 不能为空` }]}
+          >
+            <Input placeholder={`请输入 ${param} 参数`} />
+          </Form.Item>
+        ))}
+      </Form>
+    </Modal>
+  );
+});
+
+MailTemplateSendForm.displayName = 'MailTemplateSendForm';
+
+export default MailTemplateSendForm;
+

+ 198 - 0
src/components/notification/EmailNotifyForm.tsx

@@ -0,0 +1,198 @@
+import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react';
+import { Modal, Form, Input, Select, Row, Col, message } from 'antd';
+import { mailNotifyConfigApi, MailNotifyConfigVO } from '../../api/mailNotifyConfig';
+import { emailTemplateApi } from '../../api/emailTemplate';
+
+interface EmailNotifyFormProps {
+  onSuccess?: () => void;
+}
+
+export interface EmailNotifyFormRef {
+  open: (type: string, id?: number) => void;
+}
+
+const EmailNotifyForm = forwardRef<EmailNotifyFormRef, EmailNotifyFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [dialogTitle, setDialogTitle] = useState('');
+  const [formLoading, setFormLoading] = useState(false);
+  const [formType, setFormType] = useState<'create' | 'update'>('create');
+  const [form] = Form.useForm();
+  const [templateList, setTemplateList] = useState<Array<{ label: string; value: string }>>([]);
+
+  // 时间单位配置
+  const timeUnits = [
+    { name: 'days', label: '天', options: Array.from({ length: 31 }, (_, i) => i) },
+    { name: 'hours', label: '小时', options: Array.from({ length: 24 }, (_, i) => i) },
+    { name: 'minutes', label: '分钟', options: Array.from({ length: 60 }, (_, i) => i) },
+    { name: 'seconds', label: '秒', options: Array.from({ length: 60 }, (_, i) => i) },
+  ];
+
+  const [timeValues, setTimeValues] = useState({
+    days: 0,
+    hours: 0,
+    minutes: 0,
+    seconds: 0,
+  });
+
+  // 打开弹窗
+  const open = async (type: string, id?: number) => {
+    setDialogVisible(true);
+    setDialogTitle(type === 'create' ? '新增邮件提醒' : '编辑邮件提醒');
+    setFormType(type as 'create' | 'update');
+    form.resetFields();
+    setTimeValues({ days: 0, hours: 0, minutes: 0, seconds: 0 });
+
+    // 加载邮件模板列表
+    try {
+      const response = await emailTemplateApi.getMailTemplatePage({
+        pageNo: 1,
+        pageSize: -1,
+      });
+      setTemplateList(
+        response.list.map((item) => ({
+          label: item.name,
+          value: item.code,
+        }))
+      );
+    } catch (error) {
+      console.error('加载邮件模板列表失败:', error);
+      setTemplateList([]);
+    }
+
+    // 修改时,设置数据
+    if (id) {
+      setFormLoading(true);
+      try {
+        const data = await mailNotifyConfigApi.getIsMailNotifyConfigById(id);
+        form.setFieldsValue({
+          configId: data.configId,
+          name: data.name || data.configName,
+          templateCode: data.templateCode,
+          status: data.status || '1',
+        });
+
+        // 转换时间
+        const totalSeconds = data.reminderTime || 0;
+        setTimeValues({
+          days: Math.floor(totalSeconds / (24 * 60 * 60)),
+          hours: Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60)),
+          minutes: Math.floor((totalSeconds % (60 * 60)) / 60),
+          seconds: totalSeconds % 60,
+        });
+      } catch (error: any) {
+        message.error(error.message || '获取邮件提醒详情失败');
+      } finally {
+        setFormLoading(false);
+      }
+    }
+  };
+
+  useImperativeHandle(ref, () => ({
+    open,
+  }));
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      const values = await form.validateFields();
+
+      setFormLoading(true);
+      try {
+        // 计算总秒数
+        const totalSeconds =
+          ((timeValues.days * 24 + timeValues.hours) * 60 + timeValues.minutes) * 60 + timeValues.seconds;
+
+        const data: MailNotifyConfigVO = {
+          ...values,
+          reminderTime: totalSeconds,
+        };
+
+        if (formType === 'create') {
+          await mailNotifyConfigApi.addIsMailNotifyConfig(data);
+          message.success('创建成功');
+        } else {
+          await mailNotifyConfigApi.updateIsMailNotifyConfig(data);
+          message.success('更新成功');
+        }
+        setDialogVisible(false);
+        onSuccess?.();
+      } catch (error: any) {
+        message.error(error.message || '操作失败');
+      } finally {
+        setFormLoading(false);
+      }
+    } catch (error) {
+      // 表单验证失败
+    }
+  };
+
+  return (
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={800}
+      destroyOnClose
+    >
+      <Form
+        form={form}
+        labelCol={{ span: 6 }}
+        wrapperCol={{ span: 18 }}
+        initialValues={{
+          status: '1',
+        }}
+      >
+        <Form.Item
+          label="提醒事项"
+          name="name"
+          rules={[{ required: true, message: '提醒事项不能为空' }]}
+        >
+          <Input placeholder="请输入提醒事项" />
+        </Form.Item>
+
+        <Form.Item
+          label="邮件模板"
+          name="templateCode"
+          rules={[{ required: true, message: '邮件模板不能为空' }]}
+        >
+          <Select placeholder="请选择邮件模板" options={templateList} />
+        </Form.Item>
+
+        <Form.Item label="提醒时长" required>
+          <Row gutter={[16, 0]}>
+            {timeUnits.map((unit, index) => (
+              <Col key={index} span={6}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+                  <Select
+                    value={timeValues[unit.name as keyof typeof timeValues]}
+                    onChange={(value) => {
+                      setTimeValues((prev) => ({
+                        ...prev,
+                        [unit.name]: value,
+                      }));
+                    }}
+                    style={{ width: '100%', minWidth: 100 }}
+                  >
+                    {unit.options.map((option) => (
+                      <Select.Option key={option} value={option}>
+                        {option}
+                      </Select.Option>
+                    ))}
+                  </Select>
+                  <span style={{ fontSize: '14px', color: '#333', whiteSpace: 'nowrap' }}>{unit.label}</span>
+                </div>
+              </Col>
+            ))}
+          </Row>
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+});
+
+EmailNotifyForm.displayName = 'EmailNotifyForm';
+
+export default EmailNotifyForm;
+

+ 313 - 0
src/components/notification/EmailNotifyManagement.tsx

@@ -0,0 +1,313 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Search, Plus, RefreshCw, Settings, ArrowLeft, Edit2, Trash2 } from 'lucide-react';
+import { mailNotifyConfigApi, MailNotifyConfigVO } from '../../api/mailNotifyConfig';
+import { toast } from 'sonner';
+import { Modal, Table, Input, Button, Switch, Space } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { Button as UIButton } from '../ui/button';
+import type { ColumnsType } from 'antd/es/table';
+import EmailNotifyForm, { EmailNotifyFormRef } from './EmailNotifyForm';
+import MailTemplateManagement from '../mailTemplate/MailTemplateManagement';
+
+export default function EmailNotifyManagement() {
+  console.log('EmailNotifyManagement 组件开始渲染');
+  const [showTemplateManagement, setShowTemplateManagement] = useState(false);
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<MailNotifyConfigVO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState({
+    pageNo: 1,
+    pageSize: 10,
+    name: '',
+    templateName: '',
+  });
+
+  // 子组件引用
+  const formRef = useRef<EmailNotifyFormRef>(null);
+
+  // 获取列表
+  const getList = async (params?: typeof queryParams) => {
+    const currentParams = params || queryParams;
+    console.log('EmailNotifyManagement: 开始获取列表,参数:', currentParams);
+    setLoading(true);
+    try {
+      const response = await mailNotifyConfigApi.listIsMailNotifyConfigPage(currentParams);
+      console.log('EmailNotifyManagement: API 响应:', response);
+      setList(response.list || []);
+      setTotal(response.total || 0);
+    } catch (error: any) {
+      console.error('EmailNotifyManagement: 获取列表失败:', error);
+      toast.error(error.message || '获取邮件提醒列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    getList();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [queryParams.pageNo, queryParams.pageSize]);
+
+  // 搜索
+  const handleQuery = () => {
+    const newParams = { ...queryParams, pageNo: 1 };
+    setQueryParams(newParams);
+    getList(newParams);
+  };
+
+  // 重置搜索
+  const resetQuery = () => {
+    const resetParams = {
+      pageNo: 1,
+      pageSize: 10,
+      name: '',
+      templateName: '',
+    };
+    setQueryParams(resetParams);
+    getList(resetParams);
+  };
+
+  // 打开表单弹窗
+  const openForm = (type: string, id?: number) => {
+    formRef.current?.open(type, id);
+  };
+
+  // 删除
+  const handleDelete = (id: number) => {
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: '确定要删除这条邮件提醒配置吗?',
+      okText: '确定',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await mailNotifyConfigApi.deleteIsMailNotifyConfig(id);
+          toast.success('删除成功');
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 状态修改
+  const handleStatusChange = async (checked: boolean, record: MailNotifyConfigVO) => {
+    try {
+      const data: MailNotifyConfigVO = {
+        ...record,
+        status: checked ? '1' : '0',
+      };
+      await mailNotifyConfigApi.updateIsMailNotifyConfig(data);
+      toast.success(checked ? '激活成功' : '取消激活成功');
+      await getList();
+    } catch (error: any) {
+      toast.error(error.message || '状态修改失败');
+      // 恢复原状态
+      await getList();
+    }
+  };
+
+  // 格式化时间
+  const formattedTime = (totalSeconds?: number) => {
+    if (!totalSeconds) return '-';
+    const days = Math.floor(totalSeconds / (24 * 60 * 60));
+    const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
+    const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
+    const seconds = totalSeconds % 60;
+    const parts = [];
+    if (days > 0) parts.push(`${days}天`);
+    if (hours > 0) parts.push(`${hours}小时`);
+    if (minutes > 0) parts.push(`${minutes}分钟`);
+    if (seconds > 0) parts.push(`${seconds}秒`);
+    return parts.length > 0 ? parts.join(' ') : '0秒';
+  };
+
+  // 表格列配置
+  const columns: ColumnsType<MailNotifyConfigVO> = [
+    {
+      title: '序号',
+      width: 80,
+      align: 'center',
+      render: (_: any, __: MailNotifyConfigVO, index: number) => {
+        return (queryParams.pageNo - 1) * queryParams.pageSize + index + 1;
+      },
+    },
+    {
+      title: '提醒事项',
+      dataIndex: 'name',
+      key: 'name',
+      align: 'center',
+    },
+    {
+      title: '是否激活',
+      dataIndex: 'status',
+      width: 120,
+      align: 'center',
+      render: (status: string | undefined, record: MailNotifyConfigVO) => {
+        const isChecked = status === '1';
+        return (
+          <Switch
+            checked={isChecked}
+            checkedChildren="ON"
+            unCheckedChildren="OFF"
+            onChange={(checked) => handleStatusChange(checked, record)}
+          />
+        );
+      },
+    },
+    {
+      title: '邮件模板编码',
+      dataIndex: 'templateCode',
+      key: 'templateCode',
+      align: 'center',
+    },
+    {
+      title: '提醒时长',
+      key: 'reminderTime',
+      align: 'center',
+      render: (_: any, record: MailNotifyConfigVO) => {
+        return formattedTime(record.reminderTime);
+      },
+    },
+    {
+      title: '操作',
+      width: 150,
+      align: 'center',
+      fixed: 'right',
+      render: (_: any, record: MailNotifyConfigVO) => {
+        return (
+          <div className="flex items-center gap-2 justify-center">
+            <UIButton
+              variant="ghost"
+              size="sm"
+              onClick={() => openForm('update', record.configId)}
+              className="h-8 px-2"
+            >
+              <Edit2 className="w-4 h-4" />
+              <span className="ml-1">编辑</span>
+            </UIButton>
+            <UIButton
+              variant="ghost"
+              size="sm"
+              onClick={() => handleDelete(record.configId!)}
+              className="h-8 px-2 text-red-600 hover:text-red-700"
+            >
+              <Trash2 className="w-4 h-4" />
+              <span className="ml-1">删除</span>
+            </UIButton>
+          </div>
+        );
+      },
+    },
+  ];
+
+  // 如果显示邮件模板管理,直接返回模板管理组件(不显示邮件提醒的搜索栏)
+  if (showTemplateManagement) {
+    return <MailTemplateManagement onBack={() => setShowTemplateManagement(false)} />;
+  }
+
+  return (
+    <div className="space-y-6">
+      {/* 搜索栏 */}
+      <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-6">
+        <div className="flex items-center gap-4 flex-wrap">
+          <div className="flex items-center gap-2 flex-1 min-w-[200px]">
+            <label className="text-sm text-gray-700 whitespace-nowrap">提醒事项:</label>
+            <Input
+              placeholder="请输入提醒事项"
+              value={queryParams.name}
+              onChange={(e) => setQueryParams({ ...queryParams, name: e.target.value })}
+              onPressEnter={handleQuery}
+              className="flex-1 max-w-[240px]"
+              allowClear
+            />
+          </div>
+          <div className="flex items-center gap-2 flex-1 min-w-[200px]">
+            <label className="text-sm text-gray-700 whitespace-nowrap">邮件模板:</label>
+            <Input
+              placeholder="请输入邮件模板"
+              value={queryParams.templateName}
+              onChange={(e) => setQueryParams({ ...queryParams, templateName: e.target.value })}
+              onPressEnter={handleQuery}
+              className="flex-1 max-w-[240px]"
+              allowClear
+            />
+          </div>
+          <div className="flex items-center gap-2">
+            <Button
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
+              onClick={handleQuery}
+            >
+              搜索
+            </Button>
+            <Button icon={<RefreshCw className="w-4 h-4" />} onClick={resetQuery}>
+              重置
+            </Button>
+            <Button
+              type="primary"
+              icon={<Plus className="w-4 h-4" />}
+              onClick={() => openForm('create')}
+            >
+              新增
+            </Button>
+            <Button
+              icon={<Settings className="w-4 h-4" />}
+              onClick={() => setShowTemplateManagement(true)}
+            >
+              设置邮件模板
+            </Button>
+          </div>
+        </div>
+      </div>
+
+      {/* 表格 */}
+      <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
+        <Table
+          columns={columns}
+          dataSource={list}
+          rowKey="configId"
+          loading={loading}
+          pagination={false}
+          scroll={{ x: 'max-content' }}
+        />
+        {/* 分页 */}
+        {!loading && list.length > 0 && (
+          <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
+            <div className="flex items-center justify-between">
+              <div className="text-sm text-gray-600">
+                共 <span className="text-blue-600 font-medium">{total}</span> 条记录
+              </div>
+              <div className="flex items-center gap-2">
+                <Button
+                  size="small"
+                  disabled={queryParams.pageNo === 1}
+                  onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
+                >
+                  上一页
+                </Button>
+                <span className="text-sm text-gray-600">
+                  第 {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize)} 页
+                </span>
+                <Button
+                  size="small"
+                  disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
+                  onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
+                >
+                  下一页
+                </Button>
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+
+      {/* 表单弹窗 */}
+      <EmailNotifyForm ref={formRef} onSuccess={getList} />
+    </div>
+  );
+}
+

+ 8 - 0
src/components/user/UserForm.tsx

@@ -258,6 +258,14 @@ const UserForm = forwardRef<UserFormRef, UserFormProps>(({ onSuccess }, ref) =>
         </Row>
 
         <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item
+              label="卡号"
+              name="cardNfc"
+            >
+              <Input placeholder="请输入卡号(选填)" />
+            </Form.Item>
+          </Col>
           <Col span={12}>
             <Form.Item label="备注" name="remark">
               <TextArea rows={3} placeholder="请输入内容" />

+ 1 - 0
src/types/index.ts

@@ -60,6 +60,7 @@ export interface UserVO {
   remark?: string;
   loginDate?: Date | string;
   createTime?: Date | string;
+  cardNfc?: string;
 }
 
 // 角色 VO 类型