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:
parent
5c93ae49b7
commit
5c262d2f45
71 changed files with 7623 additions and 0 deletions
649
src/components/AdminPanel.tsx
Normal file
649
src/components/AdminPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue