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 (
{/* 顶部导航 */}
{errorMsg && (
)}
{/* 核心指标卡片 */}
{/* 第一排图表:硬性指标 */}
{analytics.degreeData.map((entry, index) => (
|
))}
{
analytics.tenureData.map((entry, index) => (
|
))
}
{/* 第二排图表:软性特质 */}
{/* 第三排图表:组织与背景 (新模块) */}
{/* 部门分布 */}
{/* 性别比例 */}
`${name} ${(percent * 100).toFixed(0)}%`}
>
{analytics.genderData.map((entry, index) => (
|
))}
{/* 院校背景 */}
{analytics.schoolTierData.map((entry, index) => (
|
))}
);
}