stud-manager / pages /Settings.tsx
dvc890's picture
Upload 53 files
842b4cd verified
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">
此操作将自动把所有学生升级到下一年级 (如: 一年级 &rarr; 二年级)。<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>
);
};