|
|
@@ -0,0 +1,441 @@
|
|
|
+import React, { useState } from 'react';
|
|
|
+import { Plus, Search, Edit2, Trash2, MoreVertical, Mail, Phone, MessageSquare, Smartphone } from 'lucide-react';
|
|
|
+
|
|
|
+interface TableRow {
|
|
|
+ id: number;
|
|
|
+ [key: string]: any;
|
|
|
+}
|
|
|
+
|
|
|
+export default function NotificationManagement() {
|
|
|
+ const [searchTerm, setSearchTerm] = useState('');
|
|
|
+ const [showAddModal, setShowAddModal] = useState(false);
|
|
|
+ const [editingItem, setEditingItem] = useState<TableRow | null>(null);
|
|
|
+ const [notificationType, setNotificationType] = useState<string>('email'); // 通知类型:email, sms, message, app
|
|
|
+
|
|
|
+ // 通知管理数据
|
|
|
+ const notificationData: TableRow[] = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ title: '系统升级通知',
|
|
|
+ content: '系统将于本周六凌晨2:00-4:00进行升级维护,期间系统将暂停服务',
|
|
|
+ type: '系统通知',
|
|
|
+ level: '重要',
|
|
|
+ sender: '系统管理员',
|
|
|
+ target: '全体用户',
|
|
|
+ status: '已发送',
|
|
|
+ readCount: 156,
|
|
|
+ totalCount: 200,
|
|
|
+ sendTime: '2025-12-01 10:00',
|
|
|
+ createTime: '2025-12-01 09:30'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ title: '设备异常告警',
|
|
|
+ content: '1号变压器出现异常,请相关人员立即检处理',
|
|
|
+ type: '告警通知',
|
|
|
+ level: '紧急',
|
|
|
+ sender: '监控系统',
|
|
|
+ target: '技术部、运维部',
|
|
|
+ status: '已发送',
|
|
|
+ readCount: 45,
|
|
|
+ totalCount: 50,
|
|
|
+ sendTime: '2025-12-04 08:30',
|
|
|
+ createTime: '2025-12-04 08:25'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ title: '月度安全培训通知',
|
|
|
+ content: '本月安全培训将于12月10日14:00在会议室举行,请全体员工准时参加',
|
|
|
+ type: '培训通知',
|
|
|
+ level: '普通',
|
|
|
+ sender: '安全部',
|
|
|
+ target: '全体用户',
|
|
|
+ status: '定时发送',
|
|
|
+ readCount: 0,
|
|
|
+ totalCount: 200,
|
|
|
+ sendTime: '2025-12-10 09:00',
|
|
|
+ createTime: '2025-12-03 16:00'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ title: '隔离作业审批通过',
|
|
|
+ content: '您提交的2号配电柜维护隔离作业已通过审批,可以开始执行',
|
|
|
+ type: '审批通知',
|
|
|
+ level: '普通',
|
|
|
+ sender: '审批系统',
|
|
|
+ target: '张三',
|
|
|
+ status: '已发送',
|
|
|
+ readCount: 1,
|
|
|
+ totalCount: 1,
|
|
|
+ sendTime: '2025-12-03 15:30',
|
|
|
+ createTime: '2025-12-03 15:28'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ title: '密码过期提醒',
|
|
|
+ content: '您的密码将于7天后过期,请及时修改密码',
|
|
|
+ type: '安全提醒',
|
|
|
+ level: '普通',
|
|
|
+ sender: '系统管理员',
|
|
|
+ target: '李四、王五',
|
|
|
+ status: '已发送',
|
|
|
+ readCount: 2,
|
|
|
+ totalCount: 2,
|
|
|
+ sendTime: '2025-12-04 09:00',
|
|
|
+ createTime: '2025-12-04 08:55'
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 表格列配置
|
|
|
+ const columns = [
|
|
|
+ { key: 'title', label: '通知标题', width: '20%' },
|
|
|
+ { key: 'type', label: '类型', width: '10%' },
|
|
|
+ { key: 'level', label: '级别', width: '8%' },
|
|
|
+ { key: 'sender', label: '发送人', width: '10%' },
|
|
|
+ { key: 'target', label: '接收对象', width: '12%' },
|
|
|
+ { key: 'status', label: '状态', width: '10%' },
|
|
|
+ { key: 'readCount', label: '已读/总数', width: '10%' },
|
|
|
+ { key: 'sendTime', label: '发送时间', width: '15%' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 过滤数据
|
|
|
+ const filteredData = notificationData.filter((item) =>
|
|
|
+ Object.values(item).some((value) =>
|
|
|
+ String(value).toLowerCase().includes(searchTerm.toLowerCase())
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ const handleDelete = (id: number) => {
|
|
|
+ if (confirm('确定要删除这条数据吗?')) {
|
|
|
+ console.log('删除:', id);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleEdit = (item: TableRow) => {
|
|
|
+ setEditingItem(item);
|
|
|
+ setShowAddModal(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 渲染表格内容
|
|
|
+ const renderTable = () => (
|
|
|
+ <>
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <table className="w-full">
|
|
|
+ <thead>
|
|
|
+ <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
|
|
|
+ 序号
|
|
|
+ </th>
|
|
|
+ {columns.map((column) => (
|
|
|
+ <th
|
|
|
+ key={column.key}
|
|
|
+ className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider"
|
|
|
+ style={{ width: column.width }}
|
|
|
+ >
|
|
|
+ {column.label}
|
|
|
+ </th>
|
|
|
+ ))}
|
|
|
+ <th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
+ 操作
|
|
|
+ </th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody className="divide-y divide-gray-100">
|
|
|
+ {filteredData.map((row, index) => (
|
|
|
+ <tr
|
|
|
+ key={row.id}
|
|
|
+ className="hover:bg-blue-50/30 transition-colors"
|
|
|
+ >
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
+ {index + 1}
|
|
|
+ </td>
|
|
|
+ {columns.map((column) => (
|
|
|
+ <td key={column.key} className="px-6 py-4 text-sm text-gray-900">
|
|
|
+ {column.key === 'status' ? (
|
|
|
+ <span
|
|
|
+ className={`inline-flex px-3 py-1 rounded-lg text-xs ${
|
|
|
+ row[column.key] === '已发送'
|
|
|
+ ? 'bg-green-100 text-green-700'
|
|
|
+ : row[column.key] === '定时发送'
|
|
|
+ ? 'bg-blue-100 text-blue-700'
|
|
|
+ : 'bg-gray-100 text-gray-700'
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ {row[column.key]}
|
|
|
+ </span>
|
|
|
+ ) : column.key === 'level' ? (
|
|
|
+ <span
|
|
|
+ className={`inline-flex px-3 py-1 rounded-lg text-xs ${
|
|
|
+ row[column.key] === '紧急'
|
|
|
+ ? 'bg-red-100 text-red-700'
|
|
|
+ : row[column.key] === '重要'
|
|
|
+ ? 'bg-orange-100 text-orange-700'
|
|
|
+ : 'bg-blue-100 text-blue-700'
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ {row[column.key]}
|
|
|
+ </span>
|
|
|
+ ) : column.key === 'readCount' ? (
|
|
|
+ <span className="text-gray-900">
|
|
|
+ {row.readCount}/{row.totalCount}
|
|
|
+ </span>
|
|
|
+ ) : (
|
|
|
+ row[column.key]
|
|
|
+ )}
|
|
|
+ </td>
|
|
|
+ ))}
|
|
|
+ <td className="px-6 py-4">
|
|
|
+ <div className="flex items-center justify-center gap-2">
|
|
|
+ <button
|
|
|
+ onClick={() => handleEdit(row)}
|
|
|
+ className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
|
|
+ title="编辑"
|
|
|
+ >
|
|
|
+ <Edit2 className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={() => handleDelete(row.id)}
|
|
|
+ className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
|
|
+ title="删除"
|
|
|
+ >
|
|
|
+ <Trash2 className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
+ title="更多"
|
|
|
+ >
|
|
|
+ <MoreVertical className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ ))}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 分页 */}
|
|
|
+ <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="text-sm text-gray-600">
|
|
|
+ 共 <span className="text-blue-600">{filteredData.length}</span> 条记录
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
+ 上一页
|
|
|
+ </button>
|
|
|
+ <button className="px-4 py-2 text-sm text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors">
|
|
|
+ 1
|
|
|
+ </button>
|
|
|
+ <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
+ 2
|
|
|
+ </button>
|
|
|
+ <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
+ 3
|
|
|
+ </button>
|
|
|
+ <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
+ 下一页
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+
|
|
|
+ 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>
|
|
|
+ </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>
|
|
|
+
|
|
|
+ <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>
|
|
|
+ {notificationType === 'email' && renderTable()}
|
|
|
+ {notificationType === 'sms' && renderTable()}
|
|
|
+ {notificationType === 'message' && renderTable()}
|
|
|
+ {notificationType === 'app' && renderTable()}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 新增/编辑弹窗 */}
|
|
|
+ {showAddModal && (
|
|
|
+ <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
|
|
|
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto animate-in zoom-in duration-200">
|
|
|
+ {/* 弹窗标题 */}
|
|
|
+ <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
|
|
|
+ <h3 className="text-lg text-gray-900">
|
|
|
+ {editingItem ? '编辑通知' : '新增通知'}
|
|
|
+ </h3>
|
|
|
+ <button
|
|
|
+ onClick={() => setShowAddModal(false)}
|
|
|
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
+ >
|
|
|
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 弹窗内容 */}
|
|
|
+ <div className="px-6 py-6">
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">通知标题 *</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ className="w-full px-4 py-2.5 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"
|
|
|
+ placeholder="请输入通知标题"
|
|
|
+ defaultValue={editingItem?.title || ''}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="grid grid-cols-3 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">通知类型 *</label>
|
|
|
+ <select className="w-full px-4 py-2.5 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">
|
|
|
+ <option value="">请选择</option>
|
|
|
+ <option value="系统通知">系统通知</option>
|
|
|
+ <option value="告警通知">告警通知</option>
|
|
|
+ <option value="培训通知">培训通知</option>
|
|
|
+ <option value="审批通知">审批通知</option>
|
|
|
+ <option value="安全提醒">安全提醒</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">优先级 *</label>
|
|
|
+ <select className="w-full px-4 py-2.5 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">
|
|
|
+ <option value="">请选择</option>
|
|
|
+ <option value="紧急">紧急</option>
|
|
|
+ <option value="重要">重要</option>
|
|
|
+ <option value="普通">普通</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">发送方式</label>
|
|
|
+ <select className="w-full px-4 py-2.5 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">
|
|
|
+ <option value="立即发送">立即发送</option>
|
|
|
+ <option value="定时发送">定时发送</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">接收对象 *</label>
|
|
|
+ <select className="w-full px-4 py-2.5 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">
|
|
|
+ <option value="">请选择</option>
|
|
|
+ <option value="全体用户">全体用户</option>
|
|
|
+ <option value="按部门">按部门</option>
|
|
|
+ <option value="按角色">按角色</option>
|
|
|
+ <option value="指定用户">指定用户</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm text-gray-700 mb-2">通知内容 *</label>
|
|
|
+ <textarea
|
|
|
+ rows={5}
|
|
|
+ className="w-full px-4 py-2.5 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 resize-none"
|
|
|
+ placeholder="请输入通知内容"
|
|
|
+ defaultValue={editingItem?.content || ''}
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 弹窗底部 */}
|
|
|
+ <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
|
|
|
+ <button
|
|
|
+ onClick={() => setShowAddModal(false)}
|
|
|
+ className="px-5 py-2.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={() => {
|
|
|
+ console.log('保存:', editingItem);
|
|
|
+ setShowAddModal(false);
|
|
|
+ }}
|
|
|
+ className="px-5 py-2.5 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all"
|
|
|
+ >
|
|
|
+ {editingItem ? '保存修改' : '确定'}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|