Sets up the project structure, including essential configuration files (eslint, postcss, tailwind), initial React components, and core UI component library (shadcn/ui). Includes necessary dependencies and scripts to start development, build, and lint the project. Ensures correct setup of React Router and a basic App structure. Does this address a real problem or an imagined one? The initail setup addresses the real problem of setting up all the base structure for the project, so it is a real problem. Is there a simpler way to do this? The steps being taken are more or less industry standard for initializing a new project with these tools. What will break? None of these changes will break anything; this is a new project setup.
649 lines
No EOL
23 KiB
TypeScript
649 lines
No EOL
23 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
||
import { JobPosition } from '@/data/jobs';
|
||
import { JobDataManager } from '@/lib/dataManager';
|
||
import { SecurityManager, validateAccessCode } from '@/lib/security';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||
import { useToast } from '@/hooks/use-toast';
|
||
import { Shield, Upload, Download, History, Plus, Edit2, Trash2, X, AlertTriangle, FileText, FileSpreadsheet } from 'lucide-react';
|
||
|
||
interface AdminPanelProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
jobs: JobPosition[];
|
||
onJobsUpdate: (jobs: JobPosition[]) => void;
|
||
}
|
||
|
||
export function AdminPanel({ isOpen, onClose, jobs, onJobsUpdate }: AdminPanelProps) {
|
||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||
const [accessCode, setAccessCode] = useState('');
|
||
const [isValidating, setIsValidating] = useState(false);
|
||
const [editingJob, setEditingJob] = useState<JobPosition | null>(null);
|
||
const [isJobDialogOpen, setIsJobDialogOpen] = useState(false);
|
||
const [showDeleteAllDialog, setShowDeleteAllDialog] = useState(false);
|
||
const [deleteAllConfirmText, setDeleteAllConfirmText] = useState('');
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const { toast } = useToast();
|
||
|
||
// Handle authentication
|
||
const handleLogin = async () => {
|
||
if (SecurityManager.isLocked()) {
|
||
const remainingTime = Math.ceil(SecurityManager.getRemainingLockTime() / 60000);
|
||
toast({
|
||
title: "访问被锁定",
|
||
description: `请 ${remainingTime} 分钟后再试`,
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
|
||
setIsValidating(true);
|
||
try {
|
||
const isValid = await validateAccessCode(accessCode);
|
||
if (isValid) {
|
||
setIsAuthenticated(true);
|
||
SecurityManager.resetAttempts();
|
||
toast({
|
||
title: "认证成功",
|
||
description: "欢迎进入管理面板"
|
||
});
|
||
} else {
|
||
SecurityManager.recordFailedAttempt();
|
||
const attempts = SecurityManager.getFailedAttempts();
|
||
toast({
|
||
title: "访问码错误",
|
||
description: `剩余尝试次数: ${3 - attempts}`,
|
||
variant: "destructive"
|
||
});
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
title: "认证失败",
|
||
description: "系统错误,请重试",
|
||
variant: "destructive"
|
||
});
|
||
}
|
||
setIsValidating(false);
|
||
setAccessCode('');
|
||
};
|
||
|
||
// Handle file import
|
||
const handleFileImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = event.target.files?.[0];
|
||
if (!file) return;
|
||
|
||
try {
|
||
let importedJobs: JobPosition[];
|
||
|
||
if (file.name.endsWith('.json')) {
|
||
importedJobs = await JobDataManager.importFromJSON(file);
|
||
} else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
|
||
importedJobs = await JobDataManager.importFromXLSX(file);
|
||
} else {
|
||
throw new Error('不支持的文件格式,请使用 JSON 或 Excel 文件');
|
||
}
|
||
|
||
const updatedJobs = [...jobs, ...importedJobs];
|
||
onJobsUpdate(updatedJobs);
|
||
JobDataManager.saveJobs(updatedJobs);
|
||
|
||
toast({
|
||
title: "导入成功",
|
||
description: `成功导入 ${importedJobs.length} 个职位`
|
||
});
|
||
} catch (error) {
|
||
toast({
|
||
title: "导入失败",
|
||
description: error.message,
|
||
variant: "destructive"
|
||
});
|
||
}
|
||
|
||
// Reset file input
|
||
if (fileInputRef.current) {
|
||
fileInputRef.current.value = '';
|
||
}
|
||
};
|
||
|
||
// Handle job operations
|
||
const handleAddJob = () => {
|
||
setEditingJob({
|
||
id: `job_${Date.now()}`,
|
||
title: '',
|
||
department: '',
|
||
priority: 'normal',
|
||
url: '',
|
||
description: [],
|
||
requirements: []
|
||
});
|
||
setIsJobDialogOpen(true);
|
||
};
|
||
|
||
const handleEditJob = (job: JobPosition) => {
|
||
setEditingJob({ ...job });
|
||
setIsJobDialogOpen(true);
|
||
};
|
||
|
||
const handleDeleteJob = (jobId: string) => {
|
||
const updatedJobs = jobs.filter(job => job.id !== jobId);
|
||
onJobsUpdate(updatedJobs);
|
||
JobDataManager.saveJobs(updatedJobs);
|
||
toast({
|
||
title: "删除成功",
|
||
description: "职位已删除"
|
||
});
|
||
};
|
||
|
||
const handleDeleteAllJobs = () => {
|
||
if (deleteAllConfirmText !== 'DELETE ALL') {
|
||
toast({
|
||
title: "确认文本错误",
|
||
description: "请输入 'DELETE ALL' 来确认删除",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Save current jobs to history before deleting all
|
||
JobDataManager.saveJobs(jobs);
|
||
|
||
// Clear all jobs
|
||
const emptyJobs: JobPosition[] = [];
|
||
onJobsUpdate(emptyJobs);
|
||
JobDataManager.saveJobs(emptyJobs);
|
||
|
||
setShowDeleteAllDialog(false);
|
||
setDeleteAllConfirmText('');
|
||
|
||
toast({
|
||
title: "删除成功",
|
||
description: `已删除所有 ${jobs.length} 个职位`,
|
||
variant: "destructive"
|
||
});
|
||
};
|
||
|
||
const handleSaveJob = (job: JobPosition) => {
|
||
const existingIndex = jobs.findIndex(j => j.id === job.id);
|
||
let updatedJobs: JobPosition[];
|
||
|
||
if (existingIndex >= 0) {
|
||
updatedJobs = [...jobs];
|
||
updatedJobs[existingIndex] = job;
|
||
} else {
|
||
updatedJobs = [...jobs, job];
|
||
}
|
||
|
||
onJobsUpdate(updatedJobs);
|
||
JobDataManager.saveJobs(updatedJobs);
|
||
setIsJobDialogOpen(false);
|
||
setEditingJob(null);
|
||
|
||
toast({
|
||
title: existingIndex >= 0 ? "更新成功" : "添加成功",
|
||
description: "职位信息已保存"
|
||
});
|
||
};
|
||
|
||
// Handle history restoration
|
||
const handleRestoreHistory = (timestamp: number) => {
|
||
const restoredJobs = JobDataManager.restoreFromHistory(timestamp);
|
||
if (restoredJobs) {
|
||
onJobsUpdate(restoredJobs);
|
||
JobDataManager.saveJobs(restoredJobs);
|
||
toast({
|
||
title: "恢复成功",
|
||
description: "数据已恢复到选定版本"
|
||
});
|
||
}
|
||
};
|
||
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<Dialog open={isOpen} onOpenChange={() => onClose()}>
|
||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
<Shield className="h-5 w-5" />
|
||
MiniMax AI Infra/算法 团队管理系统
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
数据管理和配置面板
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{!isAuthenticated ? (
|
||
<div className="p-6 max-w-md mx-auto">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-center">身份验证</CardTitle>
|
||
<CardDescription className="text-center">
|
||
请输入访问码以继续
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{SecurityManager.isLocked() && (
|
||
<div className="flex items-center gap-2 p-3 bg-destructive/10 text-destructive rounded-md">
|
||
<AlertTriangle className="h-4 w-4" />
|
||
<span className="text-sm">
|
||
访问已被锁定 {Math.ceil(SecurityManager.getRemainingLockTime() / 60000)} 分钟
|
||
</span>
|
||
</div>
|
||
)}
|
||
<div>
|
||
<Label htmlFor="accessCode">访问码</Label>
|
||
<Input
|
||
id="accessCode"
|
||
type="password"
|
||
value={accessCode}
|
||
onChange={(e) => setAccessCode(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||
disabled={isValidating || SecurityManager.isLocked()}
|
||
/>
|
||
</div>
|
||
<Button
|
||
onClick={handleLogin}
|
||
className="w-full"
|
||
disabled={isValidating || SecurityManager.isLocked() || !accessCode}
|
||
>
|
||
{isValidating ? '验证中...' : '登录'}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
) : (
|
||
<Tabs defaultValue="manage" className="w-full">
|
||
<TabsList className="grid w-full grid-cols-4">
|
||
<TabsTrigger value="manage">职位管理</TabsTrigger>
|
||
<TabsTrigger value="import">数据导入</TabsTrigger>
|
||
<TabsTrigger value="export">数据导出</TabsTrigger>
|
||
<TabsTrigger value="history">历史版本</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="manage" className="space-y-4">
|
||
<div className="flex justify-between items-center">
|
||
<h3 className="text-lg font-semibold">职位列表 ({jobs.length})</h3>
|
||
<div className="flex gap-2">
|
||
<Button onClick={handleAddJob}>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
添加职位
|
||
</Button>
|
||
{jobs.length > 0 && (
|
||
<Button
|
||
variant="destructive"
|
||
onClick={() => setShowDeleteAllDialog(true)}
|
||
>
|
||
<Trash2 className="h-4 w-4 mr-2" />
|
||
删除所有岗位
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||
{jobs.map((job) => (
|
||
<Card key={job.id}>
|
||
<CardContent className="p-4">
|
||
<div className="flex justify-between items-start">
|
||
<div className="flex-1">
|
||
<h4 className="font-medium">{job.title}</h4>
|
||
<p className="text-sm text-muted-foreground">
|
||
{job.department} · {
|
||
job.priority === 'urgent' ? '🔥 紧急' :
|
||
job.priority === 'high' ? '⚡ 高优' : '📋 普通'
|
||
}
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleEditJob(job)}
|
||
>
|
||
<Edit2 className="h-3 w-3" />
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleDeleteJob(job.id)}
|
||
>
|
||
<Trash2 className="h-3 w-3" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="import" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>数据导入</CardTitle>
|
||
<CardDescription>
|
||
支持 JSON 和 Excel 格式文件导入
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept=".json,.xlsx,.xls"
|
||
onChange={handleFileImport}
|
||
className="hidden"
|
||
/>
|
||
<Button
|
||
onClick={() => fileInputRef.current?.click()}
|
||
className="w-full"
|
||
>
|
||
<Upload className="h-4 w-4 mr-2" />
|
||
选择文件导入
|
||
</Button>
|
||
|
||
<div className="text-sm text-muted-foreground space-y-2">
|
||
<p><strong>支持格式:</strong></p>
|
||
<ul className="list-disc list-inside space-y-1">
|
||
<li>JSON 文件 (.json) - 标准数据格式</li>
|
||
<li>Excel 文件 (.xlsx, .xls) - 包含完整职位信息</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="export" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>数据导出</CardTitle>
|
||
<CardDescription>
|
||
导出当前职位数据用于备份或分享
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<Button
|
||
onClick={() => JobDataManager.exportToJSON(jobs)}
|
||
className="w-full"
|
||
variant="outline"
|
||
>
|
||
<FileText className="h-4 w-4 mr-2" />
|
||
导出为 JSON 文件
|
||
</Button>
|
||
<Button
|
||
onClick={() => JobDataManager.exportToXLSX(jobs)}
|
||
className="w-full"
|
||
variant="outline"
|
||
>
|
||
<FileSpreadsheet className="h-4 w-4 mr-2" />
|
||
导出为 Excel 文件
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="history" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>历史版本</CardTitle>
|
||
<CardDescription>
|
||
查看和恢复数据的历史版本
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||
{JobDataManager.getHistory().map((entry) => (
|
||
<Card key={entry.timestamp}>
|
||
<CardContent className="p-3">
|
||
<div className="flex justify-between items-center">
|
||
<div>
|
||
<p className="font-medium">
|
||
{new Date(entry.timestamp).toLocaleString('zh-CN')}
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{entry.count} 个职位
|
||
</p>
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleRestoreHistory(entry.timestamp)}
|
||
>
|
||
恢复
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
)}
|
||
|
||
{/* Job Edit Dialog */}
|
||
{editingJob && (
|
||
<JobEditDialog
|
||
job={editingJob}
|
||
isOpen={isJobDialogOpen}
|
||
onClose={() => {
|
||
setIsJobDialogOpen(false);
|
||
setEditingJob(null);
|
||
}}
|
||
onSave={handleSaveJob}
|
||
/>
|
||
)}
|
||
|
||
{/* Delete All Jobs Confirmation Dialog */}
|
||
<Dialog open={showDeleteAllDialog} onOpenChange={setShowDeleteAllDialog}>
|
||
<DialogContent className="max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||
<AlertTriangle className="h-5 w-5" />
|
||
危险操作确认
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
此操作将永久删除所有 {jobs.length} 个职位数据。此操作不可逆!
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-md">
|
||
<p className="text-sm text-destructive font-medium mb-2">
|
||
⚠️ 警告:此操作将:
|
||
</p>
|
||
<ul className="text-sm text-destructive space-y-1">
|
||
<li>• 删除所有 {jobs.length} 个职位</li>
|
||
<li>• 清空当前数据</li>
|
||
<li>• 在历史记录中保存删除前的数据</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="confirmText">
|
||
请输入 <code className="bg-muted px-1 rounded">DELETE ALL</code> 来确认删除:
|
||
</Label>
|
||
<Input
|
||
id="confirmText"
|
||
value={deleteAllConfirmText}
|
||
onChange={(e) => setDeleteAllConfirmText(e.target.value)}
|
||
placeholder="输入 DELETE ALL"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-2 mt-6">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => {
|
||
setShowDeleteAllDialog(false);
|
||
setDeleteAllConfirmText('');
|
||
}}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
variant="destructive"
|
||
onClick={handleDeleteAllJobs}
|
||
disabled={deleteAllConfirmText !== 'DELETE ALL'}
|
||
>
|
||
<Trash2 className="h-4 w-4 mr-2" />
|
||
确认删除所有
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|
||
|
||
// Job editing dialog component
|
||
interface JobEditDialogProps {
|
||
job: JobPosition;
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onSave: (job: JobPosition) => void;
|
||
}
|
||
|
||
function JobEditDialog({ job, isOpen, onClose, onSave }: JobEditDialogProps) {
|
||
const [editedJob, setEditedJob] = useState<JobPosition>(job);
|
||
|
||
const handleSave = () => {
|
||
if (!editedJob.title || !editedJob.department || !editedJob.url) {
|
||
return;
|
||
}
|
||
onSave(editedJob);
|
||
};
|
||
|
||
const updateField = (field: keyof JobPosition, value: any) => {
|
||
setEditedJob(prev => ({ ...prev, [field]: value }));
|
||
};
|
||
|
||
const updateArrayField = (field: 'description' | 'requirements' | 'bonus', value: string) => {
|
||
const array = value.split('\n').filter(Boolean);
|
||
setEditedJob(prev => ({
|
||
...prev,
|
||
[field]: field === 'bonus' ? (array.length > 0 ? array : undefined) : array
|
||
}));
|
||
};
|
||
|
||
return (
|
||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>
|
||
{job.title ? '编辑职位' : '添加职位'}
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="title">职位名称 *</Label>
|
||
<Input
|
||
id="title"
|
||
value={editedJob.title}
|
||
onChange={(e) => updateField('title', e.target.value)}
|
||
placeholder="例如:高性能网络专家"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="department">部门 *</Label>
|
||
<Input
|
||
id="department"
|
||
value={editedJob.department}
|
||
onChange={(e) => updateField('department', e.target.value)}
|
||
placeholder="例如:系统组直招"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="priority">优先级</Label>
|
||
<Select
|
||
value={editedJob.priority}
|
||
onValueChange={(value) => updateField('priority', value)}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="urgent">🔥 紧急</SelectItem>
|
||
<SelectItem value="high">⚡ 高优</SelectItem>
|
||
<SelectItem value="normal">📋 普通</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="url">申请链接 *</Label>
|
||
<Input
|
||
id="url"
|
||
value={editedJob.url}
|
||
onChange={(e) => updateField('url', e.target.value)}
|
||
placeholder="https://..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="description">职位描述</Label>
|
||
<Textarea
|
||
id="description"
|
||
rows={4}
|
||
value={editedJob.description.join('\n')}
|
||
onChange={(e) => updateArrayField('description', e.target.value)}
|
||
placeholder="每行一个职责描述"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="requirements">职位要求</Label>
|
||
<Textarea
|
||
id="requirements"
|
||
rows={4}
|
||
value={editedJob.requirements.join('\n')}
|
||
onChange={(e) => updateArrayField('requirements', e.target.value)}
|
||
placeholder="每行一个要求"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="bonus">加分项(可选)</Label>
|
||
<Textarea
|
||
id="bonus"
|
||
rows={3}
|
||
value={editedJob.bonus?.join('\n') || ''}
|
||
onChange={(e) => updateArrayField('bonus', e.target.value)}
|
||
placeholder="每行一个加分项"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-2 mt-6">
|
||
<Button variant="outline" onClick={onClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleSave}
|
||
disabled={!editedJob.title || !editedJob.department || !editedJob.url}
|
||
>
|
||
保存
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
} |