import React, { useState, useMemo } from 'react'; import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { Users, GraduationCap, Clock, Briefcase, Upload, Award, Sparkles, Brain, AlertCircle, Building2, UserCircle } from 'lucide-react'; // --- 预置真实数据集 (基于您提供的 CSV 文件) --- const DEMO_DATA = [ { name: "何隆基", enName: "Keith", dept: "产品运营中心", role: "产品总监", level: "P7", joinDate: "2021/4/26", degree: "硕士", school: "Northern Arizona University", major: "计算机科学与技术", gender: "男", mbti: "INTJ-T", zodiac: "射手座", status: "在职" }, { name: "林颖聪", enName: "Molly", dept: "Ug运营部", role: "海外运营经理", level: "S5", joinDate: "2019/6/3", degree: "本科", school: "广东外语外贸大学", major: "英语", gender: "女", mbti: "ESTP-A", zodiac: "天秤座", status: "在职" }, { name: "周一苇", enName: "Ivan", dept: "技术研发部", role: "技术项目经理", level: "T3", joinDate: "2024/6/3", degree: "本科", school: "江西理工大学", major: "计算机科学与技术", gender: "男", mbti: "ISFP-A", zodiac: "天秤座", status: "在职" }, { name: "梁伟茂", enName: "Cross", dept: "技术研发部-前端开发组", role: "前端开发工程师", level: "T3", joinDate: "2023/5/12", degree: "本科", school: "华南理工大学", major: "软件工程", gender: "男", mbti: "INTP-A", zodiac: "金牛座", status: "在职" }, { name: "唐衍宁", enName: "Robin", dept: "Ug运营部-海外运营部", role: "海外用户运营", level: "S1", joinDate: "2025/10/27", degree: "硕士", school: "法国克莱蒙高等商学院", major: "商业智能与分析", gender: "女", mbti: "INTJ-T", zodiac: "天蝎座", status: "试用期" }, { name: "何运凤", enName: "Rabbit", dept: "产品开发部-UI设计组", role: "UI设计师", level: "P2", joinDate: "2025/11/3", degree: "本科", school: "广州大学华软软件学院", major: "动画", gender: "女", mbti: "ENFP-T", zodiac: "摩羯座", status: "试用期" }, { name: "李佳田", enName: "Freya", dept: "Ug运营部-海外运营部", role: "海外用户运营", level: "S1", joinDate: "2025/11/5", degree: "硕士", school: "韩国外国语大学", major: "文化产业管理", gender: "女", mbti: "ENFJ-A", zodiac: "天秤座", status: "试用期" }, { name: "黄振华", enName: "Miles", dept: "产品开发部-电商与数据组", role: "数据分析实习生", level: "P0", joinDate: "2024/10/8", degree: "硕士", school: "广东财经大学", major: "应用统计", gender: "男", mbti: "ISTJ", zodiac: "巨蟹座", status: "试用期" }, { name: "庄凯霖", enName: "Patrick", dept: "产品开发部-AIGC项目组", role: "AIGC研究员", level: "T0", joinDate: "2024/1/19", degree: "本科", school: "华南理工大学", major: "人工智能", gender: "男", mbti: "INTP", zodiac: "处女座", status: "离职" } ]; const COLORS = ['#6366f1', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6', '#3b82f6', '#f43f5e', '#06b6d4']; const GENDER_COLORS = ['#3b82f6', '#ec4899', '#94a3b8']; // 男, 女, 未知 // --- 强大的 CSV 解析辅助函数 --- const parseCSVLine = (text) => { const result = []; let cell = ''; let inQuotes = false; for (let i = 0; i < text.length; i++) { const char = text[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { result.push(cell.trim()); cell = ''; } else { cell += char; } } result.push(cell.trim()); return result; }; // --- 数据清洗辅助函数 --- const calculateTenure = (joinDateStr) => { if (!joinDateStr) return 0; // 尝试处理 Excel 数字日期格式 (例如 45321) let joinDate; if (!isNaN(joinDateStr) && !joinDateStr.includes('-') && !joinDateStr.includes('/')) { joinDate = new Date((joinDateStr - 25569) * 86400 * 1000); } else { joinDate = new Date(joinDateStr); } if (isNaN(joinDate.getTime())) return 0; // 无效日期 const today = new Date('2026-01-22'); const diffTime = Math.abs(today - joinDate); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return (diffDays / 365).toFixed(1); }; const cleanMBTI = (mbtiStr) => { if (!mbtiStr) return "未知"; // 处理类似 INTJ-T, (ENFJ-A 的格式 const clean = mbtiStr.replace(/[^a-zA-Z]/g, '').substring(0, 4).toUpperCase(); return clean.length === 4 ? clean : "未知"; }; const getSchoolTier = (schoolName) => { if (!schoolName) return "其他高校"; const tier1 = ["清华大学", "北京大学", "复旦大学", "上海交通大学", "浙江大学", "南京大学", "中国科学技术大学", "中山大学", "华南理工大学", "哈尔滨工业大学", "西安交通大学", "武汉大学", "华中科技大学"]; // 简单的逻辑判断是否为纯中文,如果包含英文字符通常是海外(或数据源自带英文名) if (/[a-zA-Z]/.test(schoolName) || /University|College|School|Institute/i.test(schoolName)) return "海外高校"; if (tier1.some(s => schoolName.includes(s))) return "985/双一流"; return "其他高校"; }; // --- 字段映射逻辑 --- const FIELD_ALIASES = { name: ["姓名", "Name", "员工姓名"], enName: ["英文名", "English Name", "EnName"], dept: ["子部门", "部门", "Department", "Dept", "一级部门", "SubDepartment", "二级部门"], // 优先取子部门 role: ["职位", "岗位", "Role", "Position", "职务"], level: ["职级", "Level", "Rank", "等级"], joinDate: ["入职时间", "入职日期", "Join Date", "JoinDate"], degree: ["学历", "最高学历", "Degree", "Education"], school: ["毕业学校", "毕业院校", "School", "University"], major: ["专业", "Major", "所学专业"], gender: ["性别", "Gender"], mbti: ["MBTI", "mbti", "Mbti", "性格测试"], zodiac: ["星座", "Zodiac", "星盘"], status: ["在职情况", "状态", "Status", "员工状态"] }; const matchHeader = (headers, fieldKey) => { const aliases = FIELD_ALIASES[fieldKey]; let index = headers.findIndex(h => aliases.includes(h)); if (index === -1) { index = headers.findIndex(h => aliases.some(alias => h.includes(alias))); } return index; }; // --- 组件 --- const StatCard = ({ title, value, subtext, icon: Icon, color }) => (

{title}

{value}

{subtext &&

{subtext}

}
); const SectionTitle = ({ title, icon: Icon }) => (

{title}

); export default function TeamDashboard() { const [rawData, setRawData] = useState(DEMO_DATA); const [fileName, setFileName] = useState("预置数据 (2026.01.22)"); const [errorMsg, setErrorMsg] = useState(""); // --- 数据处理 Hook --- const analytics = useMemo(() => { let totalTenure = 0; let masterCount = 0; let validTenureCount = 0; const degreeDist = {}; const levelDist = {}; const mbtiDist = {}; const zodiacDist = {}; const deptDist = {}; const genderDist = { "男": 0, "女": 0 }; const tenureDist = { "< 1年": 0, "1-3年": 0, "3-5年": 0, "5年以上": 0 }; const schoolTierDist = {}; const mbtiDimension = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 }; rawData.forEach(item => { // 学历 const degree = item.degree ? item.degree.replace(/[\r\n]/g, '').trim() : "未知"; if (degree) { degreeDist[degree] = (degreeDist[degree] || 0) + 1; if (degree.includes("硕士") || degree.includes("博士") || degree.includes("Master") || degree.includes("PhD")) masterCount++; } // 部门 (清理一下长部门名) let dept = item.dept || "未知部门"; // 如果部门名包含“-”,通常取最后一段作为短名,或者保留全名 // 例如 "Ug运营部-海外运营部" -> "海外运营部" if (dept.includes('-')) { const parts = dept.split('-'); dept = parts[parts.length - 1]; } deptDist[dept] = (deptDist[dept] || 0) + 1; // 性别 if (item.gender === "男") genderDist["男"]++; else if (item.gender === "女") genderDist["女"]++; else genderDist["未知"] = (genderDist["未知"] || 0) + 1; // 职级 let levelPrefix = "其他"; if (item.level) { const cleanLevel = item.level.trim().toUpperCase(); if (cleanLevel.startsWith('P')) levelPrefix = "P序列 (产品/运营)"; else if (cleanLevel.startsWith('T')) levelPrefix = "T序列 (技术)"; else if (cleanLevel.startsWith('S')) levelPrefix = "S序列 (职能/支持)"; else if (cleanLevel.startsWith('M')) levelPrefix = "M序列 (管理)"; else levelPrefix = cleanLevel.charAt(0); } levelDist[levelPrefix] = (levelDist[levelPrefix] || 0) + 1; // 司龄 const tenure = parseFloat(calculateTenure(item.joinDate)); if (tenure >= 0) { // 含 0 totalTenure += tenure; validTenureCount++; if (tenure < 1) tenureDist["< 1年"]++; else if (tenure < 3) tenureDist["1-3年"]++; else if (tenure < 5) tenureDist["3-5年"]++; else tenureDist["5年以上"]++; } // MBTI const mbti = cleanMBTI(item.mbti); if (mbti !== "未知") { mbtiDist[mbti] = (mbtiDist[mbti] || 0) + 1; mbti.split('').forEach(char => mbtiDimension[char]++); } // 星座 let zodiac = item.zodiac ? item.zodiac.trim() : "未知"; if (zodiac === "NaN" || !zodiac) zodiac = "未知"; if (zodiac !== "未知") { zodiacDist[zodiac] = (zodiacDist[zodiac] || 0) + 1; } // 学校层级 const tier = getSchoolTier(item.school); schoolTierDist[tier] = (schoolTierDist[tier] || 0) + 1; }); return { totalCount: rawData.length, avgTenure: validTenureCount ? (totalTenure / validTenureCount).toFixed(1) : 0, masterRatio: rawData.length ? ((masterCount / rawData.length) * 100).toFixed(0) : 0, degreeData: Object.entries(degreeDist).map(([name, value]) => ({ name, value })), levelData: Object.entries(levelDist).map(([name, value]) => ({ name, value })), tenureData: Object.entries(tenureDist).map(([name, value]) => ({ name, value })), mbtiData: Object.entries(mbtiDist) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value) .slice(0, 8), zodiacData: Object.entries(zodiacDist).map(([name, value]) => ({ name, value })), schoolTierData: Object.entries(schoolTierDist).map(([name, value]) => ({ name, value })), deptData: Object.entries(deptDist).map(([name, value]) => ({ name, value })).sort((a,b) => b.value - a.value), genderData: Object.entries(genderDist).filter(([_,v]) => v > 0).map(([name, value]) => ({ name, value })), mbtiEiData: [ { name: '外向 (E)', value: mbtiDimension.E }, { name: '内向 (I)', value: mbtiDimension.I }, ], mbtiTfData: [ { name: '理性 (T)', value: mbtiDimension.T }, { name: '感性 (F)', value: mbtiDimension.F }, ] }; }, [rawData]); // --- CSV 解析逻辑 --- const handleFileUpload = (event) => { const file = event.target.files[0]; if (!file) return; setFileName(file.name); setErrorMsg(""); const reader = new FileReader(); reader.onload = (e) => { try { let text = e.target.result; if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1); const lines = text.split(/\r\n|\n/).filter(line => line.trim() !== ''); if (lines.length < 2) { setErrorMsg("文件似乎是空的或格式不正确。"); return; } let headerIndex = 0; let headers = []; for(let i=0; i h.trim()); if (tempHeaders.some(h => h.includes("姓名") || h.includes("Name"))) { headerIndex = i; headers = tempHeaders; break; } } if (headers.length === 0) headers = parseCSVLine(lines[0]).map(h => h.trim()); const fieldIndices = {}; Object.keys(FIELD_ALIASES).forEach(key => { fieldIndices[key] = matchHeader(headers, key); }); if (fieldIndices.name === -1) { setErrorMsg(`无法找到“姓名”列。检测到的表头: ${headers.join(', ')}`); } const parsedData = lines.slice(headerIndex + 1).map(line => { const values = parseCSVLine(line); if (values.length < headers.length / 2) return null; const row = {}; Object.keys(fieldIndices).forEach(key => { const index = fieldIndices[key]; if (index !== -1 && values[index]) { row[key] = values[index].replace(/^"|"$/g, '').trim(); } else { row[key] = ""; } }); return row; }).filter(row => row !== null && row.name); if (parsedData.length === 0) { setErrorMsg("解析成功,但未找到有效数据行。"); } else { setRawData(parsedData); } } catch (err) { console.error(err); setErrorMsg("文件解析出错。"); } }; reader.readAsText(file, "UTF-8"); }; return (
{/* 顶部导航 */}

团队人才全景盘点

数据来源: {fileName}

{errorMsg && (
{errorMsg}
)} {/* 核心指标卡片 */}
{/* 第一排图表:硬性指标 */}
{analytics.degreeData.map((entry, index) => ( ))}
{ analytics.tenureData.map((entry, index) => ( )) }
{/* 第二排图表:软性特质 */}
E:外向 I:内向 | T:理性 F:感性

主流人格 TOP 5

E vs I
T vs F
{/* 第三排图表:组织与背景 (新模块) */}
{/* 部门分布 */}
{/* 性别比例 */}
`${name} ${(percent * 100).toFixed(0)}%`} > {analytics.genderData.map((entry, index) => ( ))}
{/* 院校背景 */}
{analytics.schoolTierData.map((entry, index) => ( ))}
); }