Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { Save, Bell, Lock, Database, Loader2, Plus, X, Trash2, Globe, Edit, Calendar, GraduationCap } from 'lucide-react'; | |
| import { api } from '../services/api'; | |
| import { SystemConfig, UserRole } from '../types'; | |
| import { Toast, ToastState } from '../components/Toast'; | |
| import { ConfirmModal } from '../components/ConfirmModal'; | |
| export const Settings: React.FC = () => { | |
| const [loading, setLoading] = useState(false); | |
| const [fetching, setFetching] = useState(true); | |
| const [config, setConfig] = useState<SystemConfig>({ | |
| systemName: '智慧校园管理系统', | |
| allowRegister: true, | |
| allowAdminRegister: false, | |
| allowStudentRegister: true, | |
| allowPrincipalRegister: false, | |
| maintenanceMode: false, | |
| emailNotify: true, | |
| semester: '2023-2024学年 第一学期', | |
| semesters: ['2023-2024学年 第一学期', '2023-2024学年 第二学期'] | |
| }); | |
| const [newSemesterVal, setNewSemesterVal] = useState(''); | |
| const [editingIndex, setEditingIndex] = useState<number | null>(null); | |
| const [editVal, setEditVal] = useState(''); | |
| const [teacherFollows, setTeacherFollows] = useState(false); | |
| const [promoting, setPromoting] = useState(false); | |
| const [toast, setToast] = useState<ToastState>({ show: false, message: '', type: 'success' }); | |
| const [confirmModal, setConfirmModal] = useState<{isOpen: boolean, title: string, message: string, onConfirm: () => void, type?: 'confirm' | 'alert'}>({ isOpen: false, title: '', message: '', onConfirm: () => {} }); | |
| const currentUser = api.auth.getCurrentUser(); | |
| const isAdmin = currentUser?.role === UserRole.ADMIN; | |
| useEffect(() => { | |
| const loadConfig = async () => { | |
| try { | |
| const data = await api.config.get(); | |
| if (data && data.systemName) { | |
| setConfig(prev => ({ ...prev, ...data })); | |
| } | |
| } catch (e) { console.error(e); } finally { setFetching(false); } | |
| }; | |
| loadConfig(); | |
| }, []); | |
| const handleSave = async () => { | |
| setLoading(true); | |
| try { | |
| await api.config.save(config); | |
| setToast({ show: true, message: '全局系统配置已保存', type: 'success' }); | |
| } catch (e) { | |
| setToast({ show: true, message: '保存失败', type: 'error' }); | |
| } finally { setLoading(false); } | |
| }; | |
| const addSemester = () => { | |
| if (newSemesterVal.trim()) { | |
| const newVal = newSemesterVal.trim(); | |
| if (!config.semesters?.includes(newVal)) { | |
| setConfig(prev => ({ ...prev, semesters: [...(prev.semesters || []), newVal] })); | |
| } | |
| setNewSemesterVal(''); | |
| } | |
| }; | |
| const removeSemester = (idx: number) => { | |
| const list = [...(config.semesters || [])]; | |
| if (config.semester === list[idx]) { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: '操作无法完成', | |
| message: '无法删除当前正在使用的学期。请先切换学期后再试。', | |
| type: 'alert', | |
| onConfirm: () => {} | |
| }); | |
| return; | |
| } | |
| list.splice(idx, 1); | |
| setConfig(prev => ({ ...prev, semesters: list })); | |
| }; | |
| const startEdit = (idx: number, val: string) => { | |
| setEditingIndex(idx); | |
| setEditVal(val); | |
| }; | |
| const saveEdit = (idx: number) => { | |
| if(!editVal.trim()) return; | |
| const list = [...(config.semesters || [])]; | |
| if (config.semester === list[idx]) setConfig(prev => ({ ...prev, semester: editVal.trim() })); | |
| list[idx] = editVal.trim(); | |
| setConfig(prev => ({ ...prev, semesters: list })); | |
| setEditingIndex(null); | |
| }; | |
| const handlePromotion = async () => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: '高风险操作警告', | |
| message: `1. 所有学生年级将+1 (如: 一年级→二年级)\n2. 六年级/初三/高三学生将标记为“毕业”\n3. ${teacherFollows ? '班主任将随班升级 (继续带该班)' : '班主任将留在原年级 (班级变空)'}\n\n确定要执行全校升学吗?`, | |
| onConfirm: async () => { | |
| setPromoting(true); | |
| try { | |
| const res = await api.students.promote({ teacherFollows }); | |
| setToast({ show: true, message: `升学操作完成!共处理学生: ${res.count} 人。`, type: 'success' }); | |
| } catch (e: any) { | |
| setToast({ show: true, message: '操作失败: ' + e.message, type: 'error' }); | |
| } finally { setPromoting(false); } | |
| } | |
| }); | |
| }; | |
| if (fetching) return <div className="p-10 text-center text-gray-500">加载配置中...</div>; | |
| return ( | |
| <div className="space-y-6"> | |
| {toast.show && <Toast message={toast.message} type={toast.type} onClose={()=>setToast({...toast, show: false})}/>} | |
| <ConfirmModal | |
| isOpen={confirmModal.isOpen} | |
| title={confirmModal.title} | |
| message={confirmModal.message} | |
| onClose={()=>setConfirmModal({...confirmModal, isOpen: false})} | |
| onConfirm={confirmModal.onConfirm} | |
| type={confirmModal.type || 'confirm'} | |
| /> | |
| <div className="flex items-center space-x-2 bg-blue-50 text-blue-800 p-4 rounded-xl border border-blue-100"> | |
| <Globe size={20}/> | |
| <span className="font-bold">全局系统配置中心</span> | |
| <span className="text-xs bg-blue-200 text-blue-800 px-2 py-0.5 rounded-full ml-2"> | |
| {isAdmin ? '超级管理员模式' : '校长模式'} | |
| </span> | |
| </div> | |
| {/* Basic Info - Admin Only */} | |
| {isAdmin && ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> | |
| <div className="p-6 border-b border-gray-100 flex items-center space-x-3"> | |
| <div className="p-2 bg-blue-50 rounded-lg text-blue-600"> | |
| <Database size={20} /> | |
| </div> | |
| <h3 className="text-lg font-bold text-gray-800">基础信息</h3> | |
| </div> | |
| <div className="p-6 space-y-6"> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium text-gray-700">系统名称</label> | |
| <input type="text" value={config.systemName} onChange={(e) => setConfig({...config, systemName: e.target.value})} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"/> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium text-gray-700">当前生效学期</label> | |
| <div className="flex gap-2"> | |
| <select value={config.semester} onChange={(e) => setConfig({...config, semester: e.target.value})} className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none bg-white"> | |
| {(config.semesters || []).map(s => <option key={s} value={s}>{s}</option>)} | |
| </select> | |
| </div> | |
| <p className="text-xs text-gray-500">系统已自动根据当前日期计算默认学期。切换学期后,所有成就、成绩、考勤数据将关联到新学期。</p> | |
| </div> | |
| <div className="bg-gray-50 p-4 rounded-lg border border-gray-200"> | |
| <div className="flex justify-between items-center mb-2"> | |
| <label className="text-xs font-bold text-gray-500 uppercase block">学期列表管理</label> | |
| </div> | |
| <div className="space-y-2 mb-3 max-h-40 overflow-y-auto"> | |
| {(config.semesters || []).map((s, idx) => ( | |
| <div key={idx} className="flex items-center gap-2 bg-white p-2 rounded border border-gray-200"> | |
| {editingIndex === idx ? ( | |
| <> | |
| <input className="flex-1 border p-1 rounded text-sm" value={editVal} onChange={e=>setEditVal(e.target.value)} autoFocus/> | |
| <button onClick={()=>saveEdit(idx)} className="text-green-600 hover:bg-green-50 p-1 rounded"><Save size={16}/></button> | |
| <button onClick={()=>setEditingIndex(null)} className="text-gray-400 hover:bg-gray-50 p-1 rounded"><X size={16}/></button> | |
| </> | |
| ) : ( | |
| <> | |
| <span className="flex-1 text-sm text-gray-700">{s} {config.semester===s && <span className="text-xs text-blue-600 bg-blue-50 px-1 rounded ml-2">当前</span>}</span> | |
| <button onClick={()=>startEdit(idx, s)} className="text-gray-400 hover:text-blue-600 p-1"><Edit size={14}/></button> | |
| <button onClick={()=>removeSemester(idx)} className="text-gray-400 hover:text-red-600 p-1"><Trash2 size={14}/></button> | |
| </> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| <div className="flex gap-2"> | |
| <input className="flex-1 border border-gray-300 rounded px-3 py-1 text-sm" placeholder="手动添加历史学期..." value={newSemesterVal} onChange={e=>setNewSemesterVal(e.target.value)}/> | |
| <button onClick={addSemester} disabled={!newSemesterVal.trim()} className="px-3 py-1 bg-blue-600 text-white rounded text-sm disabled:opacity-50">添加</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Promotion Management - Admin & Principal */} | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> | |
| <div className="p-6 border-b border-gray-100 flex items-center space-x-3"> | |
| <div className="p-2 bg-purple-50 rounded-lg text-purple-600"> | |
| <GraduationCap size={20} /> | |
| </div> | |
| <h3 className="text-lg font-bold text-gray-800">学年与升学管理</h3> | |
| </div> | |
| <div className="p-6"> | |
| <div className="bg-red-50 border border-red-100 p-4 rounded-xl"> | |
| <h4 className="font-bold text-red-800 mb-2">一键升学 (年级晋升)</h4> | |
| <p className="text-sm text-red-700 mb-4"> | |
| 此操作将自动把所有学生升级到下一年级 (如: 一年级 → 二年级)。<br/> | |
| 最高年级学生将自动标记为“毕业”。如果新班级不存在,系统将自动创建。 | |
| </p> | |
| <div className="flex items-center gap-2 mb-4"> | |
| <input type="checkbox" id="teacherFollows" className="w-5 h-5 text-blue-600" checked={teacherFollows} onChange={e => setTeacherFollows(e.target.checked)}/> | |
| <label htmlFor="teacherFollows" className="text-sm font-bold text-gray-700">启用“班主任随班升级”</label> | |
| </div> | |
| <p className="text-xs text-gray-500 mb-4 pl-7"> | |
| * 选中: 张老师带“一年级(1)班”,升学后继续带“二年级(1)班”。<br/> | |
| * 未选: 张老师留在“一年级”,新的一年级(1)班将没有学生,二年级(1)班无班主任。 | |
| </p> | |
| <button onClick={handlePromotion} disabled={promoting} className="bg-red-600 text-white px-6 py-2 rounded-lg font-bold hover:bg-red-700 shadow-md flex items-center disabled:opacity-50"> | |
| {promoting && <Loader2 className="animate-spin mr-2"/>} | |
| 执行全校升学 | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Security & Access - Admin Only */} | |
| {isAdmin && ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> | |
| <div className="p-6 border-b border-gray-100 flex items-center space-x-3"> | |
| <div className="p-2 bg-indigo-50 rounded-lg text-indigo-600"> | |
| <Lock size={20} /> | |
| </div> | |
| <h3 className="text-lg font-bold text-gray-800">安全与访问</h3> | |
| </div> | |
| <div className="p-6 space-y-4"> | |
| <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> | |
| <div> | |
| <p className="font-medium text-gray-900">开放教师注册</p> | |
| <p className="text-sm text-gray-500">允许教师自行注册账号</p> | |
| </div> | |
| <label className="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" checked={config.allowRegister} onChange={(e) => setConfig({...config, allowRegister: e.target.checked})} className="sr-only peer" /> | |
| <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> | |
| </label> | |
| </div> | |
| <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> | |
| <div> | |
| <p className="font-medium text-gray-900">开放学生注册</p> | |
| <p className="text-sm text-gray-500">允许学生注册账号(需班主任审核)</p> | |
| </div> | |
| <label className="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" checked={config.allowStudentRegister} onChange={(e) => setConfig({...config, allowStudentRegister: e.target.checked})} className="sr-only peer" /> | |
| <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> | |
| </label> | |
| </div> | |
| <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> | |
| <div> | |
| <p className="font-medium text-gray-900">开放校长注册申请</p> | |
| <p className="text-sm text-gray-500">允许用户申请成为校长 (需管理员审核)</p> | |
| </div> | |
| <label className="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" checked={config.allowPrincipalRegister} onChange={(e) => setConfig({...config, allowPrincipalRegister: e.target.checked})} className="sr-only peer" /> | |
| <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> | |
| </label> | |
| </div> | |
| <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> | |
| <div> | |
| <p className="font-medium text-gray-900">开放管理员注册申请</p> | |
| <p className="text-sm text-gray-500">允许用户在注册时申请成为管理员 (需审核)</p> | |
| </div> | |
| <label className="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" checked={config.allowAdminRegister} onChange={(e) => setConfig({...config, allowAdminRegister: e.target.checked})} className="sr-only peer" /> | |
| <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Notifications - Admin Only */} | |
| {isAdmin && ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> | |
| <div className="p-6 border-b border-gray-100 flex items-center space-x-3"> | |
| <div className="p-2 bg-amber-50 rounded-lg text-amber-600"> | |
| <Bell size={20} /> | |
| </div> | |
| <h3 className="text-lg font-bold text-gray-800">通知设置</h3> | |
| </div> | |
| <div className="p-6 space-y-4"> | |
| <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> | |
| <div> | |
| <p className="font-medium text-gray-900">系统邮件通知</p> | |
| <p className="text-sm text-gray-500">当有新注册申请时发送邮件给管理员</p> | |
| </div> | |
| <label className="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" checked={config.emailNotify} onChange={(e) => setConfig({...config, emailNotify: e.target.checked})} className="sr-only peer" /> | |
| <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-amber-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-amber-500"></div> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="flex justify-end pt-4"> | |
| <button onClick={handleSave} disabled={loading} className="flex items-center space-x-2 px-6 py-3 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue-700 transition-colors shadow-lg shadow-blue-200"> | |
| {loading ? <Loader2 className="animate-spin" size={20} /> : <Save size={20} />} | |
| <span>保存配置</span> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |