20260701171607
This commit is contained in:
1 parent
cebbd3f92c
commit
13116a12cb
83 files changed
+6217
No files matched your search
@@ -0,0 +1,350 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useLocalStorage } from './hooks/useLocalStorage';
|
||||
import { SchoolForm } from './components/SchoolForm';
|
||||
import { MajorGroupForm } from './components/MajorGroupForm';
|
||||
import { DataTable } from './components/DataTable';
|
||||
import { Toast } from './components/Toast';
|
||||
import {
|
||||
exportToCSV,
|
||||
exportToExcel,
|
||||
importFromCSV,
|
||||
importFromExcel,
|
||||
} from './utils/importExport';
|
||||
import type { School, ToastMessage } from './types';
|
||||
import {
|
||||
Plus,
|
||||
Download,
|
||||
Upload,
|
||||
GraduationCap,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
|
||||
function App() {
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
addSchool,
|
||||
updateSchool,
|
||||
deleteSchool,
|
||||
addMajorGroup,
|
||||
updateMajorGroup,
|
||||
deleteMajorGroup,
|
||||
saveData,
|
||||
} = useLocalStorage();
|
||||
|
||||
const [showSchoolForm, setShowSchoolForm] = useState(false);
|
||||
const [editingSchool, setEditingSchool] = useState<School | null>(null);
|
||||
const [showMajorGroupForm, setShowMajorGroupForm] = useState(false);
|
||||
const [editingMajorGroup, setEditingMajorGroup] = useState<{
|
||||
schoolId: string;
|
||||
majorGroup: School['majorGroups'][0];
|
||||
} | null>(null);
|
||||
const [addingMajorGroupToSchool, setAddingMajorGroupToSchool] =
|
||||
useState<string | null>(null);
|
||||
const [toastMessages, setToastMessages] = useState<ToastMessage[]>([]);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const addToast = (type: ToastMessage['type'], message: string) => {
|
||||
const id = Date.now().toString();
|
||||
setToastMessages((prev) => [...prev, { id, type, message }]);
|
||||
};
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToastMessages((prev) => prev.filter((msg) => msg.id !== id));
|
||||
};
|
||||
|
||||
const handleAddSchool = (schoolData: Omit<School, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
addSchool(schoolData);
|
||||
setShowSchoolForm(false);
|
||||
addToast('success', '学校添加成功');
|
||||
};
|
||||
|
||||
const handleEditSchool = (schoolData: Omit<School, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
if (editingSchool) {
|
||||
updateSchool(editingSchool.id, schoolData);
|
||||
setEditingSchool(null);
|
||||
setShowSchoolForm(false);
|
||||
addToast('success', '学校信息更新成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSchool = (id: string) => {
|
||||
if (window.confirm('确定要删除这所学校及其所有专业组吗?')) {
|
||||
deleteSchool(id);
|
||||
addToast('success', '学校删除成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddMajorGroup = (majorGroupData: Omit<School['majorGroups'][0], 'id'>) => {
|
||||
if (addingMajorGroupToSchool) {
|
||||
addMajorGroup(addingMajorGroupToSchool, majorGroupData);
|
||||
setAddingMajorGroupToSchool(null);
|
||||
setShowMajorGroupForm(false);
|
||||
addToast('success', '专业组添加成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditMajorGroup = (majorGroupData: Omit<School['majorGroups'][0], 'id'>) => {
|
||||
if (editingMajorGroup) {
|
||||
updateMajorGroup(
|
||||
editingMajorGroup.schoolId,
|
||||
editingMajorGroup.majorGroup.id,
|
||||
majorGroupData
|
||||
);
|
||||
setEditingMajorGroup(null);
|
||||
setShowMajorGroupForm(false);
|
||||
addToast('success', '专业组信息更新成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteMajorGroup = (schoolId: string, majorGroupId: string) => {
|
||||
if (window.confirm('确定要删除这个专业组吗?')) {
|
||||
deleteMajorGroup(schoolId, majorGroupId);
|
||||
addToast('success', '专业组删除成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportCSV = () => {
|
||||
const csv = exportToCSV(data);
|
||||
const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `志愿意向数据_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
addToast('success', 'CSV导出成功');
|
||||
};
|
||||
|
||||
const handleExportExcel = () => {
|
||||
const blob = exportToExcel(data);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `志愿意向数据_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
addToast('success', 'Excel导出成功');
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setImporting(true);
|
||||
|
||||
try {
|
||||
let importedData: School[];
|
||||
|
||||
if (file.name.endsWith('.csv')) {
|
||||
const text = await file.text();
|
||||
importedData = importFromCSV(text);
|
||||
} else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
|
||||
importedData = await importFromExcel(file);
|
||||
} else {
|
||||
throw new Error('不支持的文件格式,请上传CSV或Excel文件');
|
||||
}
|
||||
|
||||
saveData(importedData);
|
||||
addToast('success', `成功导入${importedData.length}所学校数据`);
|
||||
} catch (error) {
|
||||
addToast('error', (error as Error).message);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-600 p-2 rounded-lg">
|
||||
<GraduationCap className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">志愿意向录入系统</h1>
|
||||
<p className="text-sm text-gray-500">管理您的志愿填报信息</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
title="刷新页面"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
|
||||
disabled={importing}
|
||||
>
|
||||
{importing && (
|
||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<Upload className="w-4 h-4" />
|
||||
导入数据
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
导出CSV
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportExcel}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
导出Excel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingSchool(null);
|
||||
setShowSchoolForm(true);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加学校
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<svg className="animate-spin h-10 w-10 text-blue-600" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
data={data}
|
||||
onEditSchool={(school) => {
|
||||
setEditingSchool(school);
|
||||
setShowSchoolForm(true);
|
||||
}}
|
||||
onDeleteSchool={handleDeleteSchool}
|
||||
onAddMajorGroup={(schoolId) => {
|
||||
setAddingMajorGroupToSchool(schoolId);
|
||||
setEditingMajorGroup(null);
|
||||
setShowMajorGroupForm(true);
|
||||
}}
|
||||
onEditMajorGroup={(schoolId, majorGroup) => {
|
||||
setEditingMajorGroup({ schoolId, majorGroup });
|
||||
setAddingMajorGroupToSchool(null);
|
||||
setShowMajorGroupForm(true);
|
||||
}}
|
||||
onDeleteMajorGroup={handleDeleteMajorGroup}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">数据统计</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-blue-50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-blue-600">{data.length}</div>
|
||||
<div className="text-sm text-blue-600">学校总数</div>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{data.reduce((sum, s) => sum + s.majorGroups.length, 0)}
|
||||
</div>
|
||||
<div className="text-sm text-green-600">专业组总数</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{data.filter((s) => s.type === '双一流').length}
|
||||
</div>
|
||||
<div className="text-sm text-purple-600">双一流院校</div>
|
||||
</div>
|
||||
<div className="bg-orange-50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{data.filter((s) => s.type === '985').length}
|
||||
</div>
|
||||
<div className="text-sm text-orange-600">985院校</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{showSchoolForm && (
|
||||
<SchoolForm
|
||||
school={editingSchool}
|
||||
onSubmit={editingSchool ? handleEditSchool : handleAddSchool}
|
||||
onCancel={() => {
|
||||
setShowSchoolForm(false);
|
||||
setEditingSchool(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showMajorGroupForm && (
|
||||
<MajorGroupForm
|
||||
majorGroup={editingMajorGroup?.majorGroup || null}
|
||||
onSubmit={editingMajorGroup ? handleEditMajorGroup : handleAddMajorGroup}
|
||||
onCancel={() => {
|
||||
setShowMajorGroupForm(false);
|
||||
setEditingMajorGroup(null);
|
||||
setAddingMajorGroupToSchool(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Toast messages={toastMessages} onRemove={removeToast} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in new issue
Block a user