diff --git a/components.json b/components.json new file mode 100644 index 0000000..1d282e6 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/index.html b/index.html new file mode 100644 index 0000000..da21422 --- /dev/null +++ b/index.html @@ -0,0 +1,56 @@ + + + + + + + + + + + MiniMax AI Infra/算法 团队 + + + + + + +
+ + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..bb81d70 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "name": "vite-project", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@devvai/devv-code-backend": "^1.0.0", + "@hookform/resolvers": "^5.0.1", + "@radix-ui/react-accordion": "^1.2.8", + "@radix-ui/react-alert-dialog": "^1.1.11", + "@radix-ui/react-aspect-ratio": "^1.1.4", + "@radix-ui/react-avatar": "^1.1.7", + "@radix-ui/react-checkbox": "^1.2.3", + "@radix-ui/react-collapsible": "^1.1.8", + "@radix-ui/react-context-menu": "^2.2.12", + "@radix-ui/react-dialog": "^1.1.11", + "@radix-ui/react-dropdown-menu": "^2.1.12", + "@radix-ui/react-hover-card": "^1.1.11", + "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-menubar": "^1.1.12", + "@radix-ui/react-navigation-menu": "^1.2.10", + "@radix-ui/react-popover": "^1.1.11", + "@radix-ui/react-progress": "^1.1.4", + "@radix-ui/react-radio-group": "^1.3.4", + "@radix-ui/react-scroll-area": "^1.2.6", + "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-separator": "^1.1.4", + "@radix-ui/react-slider": "^1.3.2", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.2.2", + "@radix-ui/react-tabs": "^1.1.9", + "@radix-ui/react-toast": "^1.2.11", + "@radix-ui/react-toggle": "^1.1.6", + "@radix-ui/react-toggle-group": "^1.1.7", + "@radix-ui/react-tooltip": "^1.2.4", + "@tanstack/react-table": "^8.21.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.503.0", + "next-themes": "^0.4.6", + "react": "^18.2.0", + "react-day-picker": "8.10.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.56.1", + "react-resizable-panels": "^2.1.8", + "react-router-dom": "^6.22.1", + "recharts": "^2.15.3", + "sonner": "^2.0.3", + "tailwind-merge": "^3.2.0", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.2.8", + "vaul": "^1.1.2", + "xlsx": "^0.18.5", + "zod": "^3.24.3", + "zustand": "^5.0.6" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/node": "^22.14.1", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.22.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.3", + "tailwindcss": "3", + "typescript": "~5.7.2", + "typescript-eslint": "^8.26.1", + "vite": "^6.3.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..72eb480 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,21 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import HomePage from "@/pages/HomePage"; +import NotFoundPage from "@/pages/NotFoundPage"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; + +function App() { + return ( + + + + } /> + } /> + + + + + ); +} + +export default App; diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx new file mode 100644 index 0000000..7dff5bc --- /dev/null +++ b/src/components/AdminPanel.tsx @@ -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(null); + const [isJobDialogOpen, setIsJobDialogOpen] = useState(false); + const [showDeleteAllDialog, setShowDeleteAllDialog] = useState(false); + const [deleteAllConfirmText, setDeleteAllConfirmText] = useState(''); + const fileInputRef = useRef(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) => { + 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 ( + onClose()}> + + + + + MiniMax AI Infra/算法 团队管理系统 + + + 数据管理和配置面板 + + + + {!isAuthenticated ? ( +
+ + + 身份验证 + + 请输入访问码以继续 + + + + {SecurityManager.isLocked() && ( +
+ + + 访问已被锁定 {Math.ceil(SecurityManager.getRemainingLockTime() / 60000)} 分钟 + +
+ )} +
+ + setAccessCode(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleLogin()} + disabled={isValidating || SecurityManager.isLocked()} + /> +
+ +
+
+
+ ) : ( + + + 职位管理 + 数据导入 + 数据导出 + 历史版本 + + + +
+

职位列表 ({jobs.length})

+
+ + {jobs.length > 0 && ( + + )} +
+
+ +
+ {jobs.map((job) => ( + + +
+
+

{job.title}

+

+ {job.department} · { + job.priority === 'urgent' ? '🔥 紧急' : + job.priority === 'high' ? '⚡ 高优' : '📋 普通' + } +

+
+
+ + +
+
+
+
+ ))} +
+
+ + + + + 数据导入 + + 支持 JSON 和 Excel 格式文件导入 + + + +
+ + + +
+

支持格式:

+
    +
  • JSON 文件 (.json) - 标准数据格式
  • +
  • Excel 文件 (.xlsx, .xls) - 包含完整职位信息
  • +
+
+
+
+
+
+ + + + + 数据导出 + + 导出当前职位数据用于备份或分享 + + + + + + + + + + + + + 历史版本 + + 查看和恢复数据的历史版本 + + + +
+ {JobDataManager.getHistory().map((entry) => ( + + +
+
+

+ {new Date(entry.timestamp).toLocaleString('zh-CN')} +

+

+ {entry.count} 个职位 +

+
+ +
+
+
+ ))} +
+
+
+
+
+ )} + + {/* Job Edit Dialog */} + {editingJob && ( + { + setIsJobDialogOpen(false); + setEditingJob(null); + }} + onSave={handleSaveJob} + /> + )} + + {/* Delete All Jobs Confirmation Dialog */} + + + + + + 危险操作确认 + + + 此操作将永久删除所有 {jobs.length} 个职位数据。此操作不可逆! + + + +
+
+

+ ⚠️ 警告:此操作将: +

+
    +
  • • 删除所有 {jobs.length} 个职位
  • +
  • • 清空当前数据
  • +
  • • 在历史记录中保存删除前的数据
  • +
+
+ +
+ + setDeleteAllConfirmText(e.target.value)} + placeholder="输入 DELETE ALL" + className="mt-2" + /> +
+
+ +
+ + +
+
+
+
+
+ ); +} + +// 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(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 ( + + + + + {job.title ? '编辑职位' : '添加职位'} + + + +
+
+
+ + updateField('title', e.target.value)} + placeholder="例如:高性能网络专家" + /> +
+ +
+ + updateField('department', e.target.value)} + placeholder="例如:系统组直招" + /> +
+ +
+ + +
+ +
+ + updateField('url', e.target.value)} + placeholder="https://..." + /> +
+
+ +
+
+ +