MiniMaxReferralHub/src/components/AdminPanel.tsx
Chever John 5c262d2f45
Initializes project with core setup
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.
2025-08-26 21:08:51 +08:00

649 lines
No EOL
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}