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.
This commit is contained in:
Chenwei Jiang 2025-08-26 21:08:51 +08:00
parent 5c93ae49b7
commit 5c262d2f45
Signed by: cheverjohn
GPG key ID: ADC4815BFE960182
71 changed files with 7623 additions and 0 deletions

View file

@ -0,0 +1,649 @@
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>
);
}