649 lines
23 KiB
TypeScript
649 lines
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>
|
|||
|
|
);
|
|||
|
|
}
|