Files
worth-calculator/components/calculator.tsx

1678 lines
70 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { Wallet, Github, FileText, Book, History, Eye , Star} from 'lucide-react'; // 添加新图标
import Link from 'next/link'; // 导入Link组件用于导航
import { useLanguage } from './LanguageContext';
import { LanguageSwitcher } from './LanguageSwitcher';
import { countryNames } from './LanguageContext';
// 定义PPP转换因子映射表
const pppFactors: Record<string, number> = {
'AF': 18.71,
'AO': 167.66,
'AL': 41.01,
'AR': 28.67,
'AM': 157.09,
'AG': 2.06,
'AU': 1.47,
'AT': 0.76,
'AZ': 0.50,
'BI': 680.41,
'BE': 0.75,
'BJ': 211.97,
'BF': 209.84,
'BD': 32.81,
'BG': 0.70,
'BH': 0.18,
'BS': 0.88,
'BA': 0.66,
'BY': 0.77,
'BZ': 1.37,
'BO': 2.60,
'BR': 2.36,
'BB': 2.24,
'BN': 0.58,
'BT': 20.11,
'BW': 4.54,
'CF': 280.19,
'CA': 1.21,
'CH': 1.14,
'CL': 418.43,
'CN': 4.19,
'CI': 245.25,
'CM': 228.75,
'CD': 911.27,
'CG': 312.04,
'CO': 1352.79,
'KM': 182.34,
'CV': 46.51,
'CR': 335.86,
'CY': 0.61,
'CZ': 12.66,
'DE': 0.75,
'DJ': 105.29,
'DM': 1.69,
'DK': 6.60,
'DO': 22.90,
'DZ': 37.24,
'EC': 0.51,
'EG': 4.51,
'ES': 0.62,
'EE': 0.53,
'ET': 12.11,
'FI': 0.84,
'FJ': 0.91,
'FR': 0.73,
'GA': 265.46,
'GB': 0.70,
'GE': 0.90,
'GH': 2.33,
'GN': 4053.64,
'GM': 17.79,
'GW': 214.86,
'GQ': 229.16,
'GR': 0.54,
'GD': 1.64,
'GT': 4.01,
'GY': 73.60,
'HK': 6.07,
'HN': 10.91,
'HR': 3.21,
'HT': 40.20,
'HU': 148.01,
'ID': 4673.65,
'IN': 21.99,
'IE': 0.78,
'IR': 30007.63,
'IQ': 507.58,
'IS': 145.34,
'IL': 3.59,
'IT': 0.66,
'JM': 72.03,
'JO': 0.29,
'JP': 102.84,
'KZ': 139.91,
'KE': 43.95,
'KG': 18.28,
'KH': 1400.09,
'KI': 1.00,
'KN': 1.92,
'KR': 861.82,
'LA': 2889.36,
'LB': 1414.91,
'LR': 0.41,
'LY': 0.48,
'LC': 1.93,
'LK': 51.65,
'LS': 5.90,
'LT': 0.45,
'LU': 0.86,
'LV': 0.48,
'MO': 5.18,
'MA': 3.92,
'MD': 6.06,
'MG': 1178.10,
'MV': 8.35,
'MX': 9.52,
'MK': 18.83,
'ML': 211.41,
'MT': 0.57,
'MM': 417.35,
'ME': 0.33,
'MN': 931.67,
'MZ': 24.05,
'MR': 12.01,
'MU': 16.52,
'MW': 298.82,
'MY': 1.57,
'NA': 7.40,
'NE': 257.60,
'NG': 144.27,
'NI': 11.75,
'NL': 0.77,
'NO': 10.03,
'NP': 33.52,
'NZ': 1.45,
'PK': 38.74,
'PA': 0.46,
'PE': 1.80,
'PH': 19.51,
'PG': 2.11,
'PL': 1.78,
'PR': 0.92,
'PT': 0.57,
'PY': 2575.54,
'PS': 0.57,
'QA': 2.06,
'RO': 1.71,
'RU': 25.88,
'RW': 339.88,
'SA': 1.61,
'SD': 21.85,
'SN': 245.98,
'SG': 0.84,
'SB': 7.08,
'SL': 2739.26,
'SV': 0.45,
'SO': 9107.78,
'RS': 41.13,
'ST': 10.94,
'SR': 3.55,
'SK': 0.53,
'SI': 0.56,
'SE': 8.77,
'SZ': 6.36,
'SC': 7.82,
'TC': 1.07,
'TD': 220.58,
'TG': 236.83,
'TH': 12.34,
'TJ': 2.30,
'TL': 0.41,
'TT': 4.15,
'TN': 0.91,
'TR': 2.13,
'TV': 1.29,
'TW': 13.85,
'TZ': 888.32,
'UG': 1321.35,
'UA': 7.69,
'UY': 28.45,
'US': 1.00,
'UZ': 2297.17,
'VC': 1.54,
'VN': 7473.67,
'VU': 110.17,
'XK': 0.33,
'ZA': 6.93,
'ZM': 5.59,
'ZW': 24.98
};
// 添加各国货币符号映射
const currencySymbols: Record<string, string> = {
'AF': '؋', // 阿富汗尼
'AL': 'L', // 阿尔巴尼亚列克
'DZ': 'د.ج', // 阿尔及利亚第纳尔
'AO': 'Kz', // 安哥拉宽扎
'AR': '$', // 阿根廷比索
'AM': '֏', // 亚美尼亚德拉姆
'AU': 'A$', // 澳大利亚元
'AT': '€', // 欧元
'AZ': '₼', // 阿塞拜疆马纳特
'BI': 'FBu', // 布隆迪法郎
'BE': '€', // 欧元
'BJ': 'CFA', // 西非法郎
'BF': 'CFA', // 西非法郎
'BD': '৳', // 孟加拉塔卡
'BG': 'лв', // 保加利亚列弗
'BH': '.د.ب', // 巴林第纳尔
'BS': 'B$', // 巴哈马元
'BA': 'KM', // 波黑可兑换马克
'BY': 'Br', // 白俄罗斯卢布
'BZ': 'BZ$', // 伯利兹元
'BO': 'Bs', // 玻利维亚诺
'BR': 'R$', // 巴西雷亚尔
'BB': 'Bds$', // 巴巴多斯元
'BN': 'B$', // 文莱元
'BT': 'Nu.', // 不丹努扎姆
'BW': 'P', // 博茨瓦纳普拉
'CA': 'C$', // 加拿大元
'CH': 'CHF', // 瑞士法郎
'CL': 'CLP$', // 智利比索
'CN': '¥', // 人民币
'CI': 'CFA', // 西非法郎
'CM': 'FCFA', // 中非法郎
'CD': 'FC', // 刚果法郎
'CG': 'FCFA', // 中非法郎
'CO': 'Col$', // 哥伦比亚比索
'CR': '₡', // 哥斯达黎加科朗
'CY': '€', // 欧元
'CZ': 'Kč', // 捷克克朗
'DE': '€', // 欧元
'DK': 'kr', // 丹麦克朗
'DO': 'RD$', // 多米尼加比索
'EC': '$', // 美元(厄瓜多尔使用美元)
'EG': 'E£', // 埃及镑
'ES': '€', // 欧元
'EE': '€', // 欧元
'ET': 'Br', // 埃塞俄比亚比尔
'FI': '€', // 欧元
'FJ': 'FJ$', // 斐济元
'FR': '€', // 欧元
'GB': '£', // 英镑
'GE': '₾', // 格鲁吉亚拉里
'GH': '₵', // 加纳塞地
'GR': '€', // 欧元
'GT': 'Q', // 危地马拉格查尔
'HK': 'HK$', // 港元
'HN': 'L', // 洪都拉斯伦皮拉
'HR': '€', // 欧元克罗地亚自2023年加入欧元区
'HU': 'Ft', // 匈牙利福林
'ID': 'Rp', // 印尼盾
'IN': '₹', // 印度卢比
'IE': '€', // 欧元
'IR': '﷼', // 伊朗里亚尔
'IQ': 'ع.د', // 伊拉克第纳尔
'IS': 'kr', // 冰岛克朗
'IL': '₪', // 以色列新谢克尔
'IT': '€', // 欧元
'JM': 'J$', // 牙买加元
'JO': 'JD', // 约旦第纳尔
'JP': '¥', // 日元
'KE': 'KSh', // 肯尼亚先令
'KR': '₩', // 韩元
'KW': 'د.ك', // 科威特第纳尔
'LB': 'L£', // 黎巴嫩镑
'LK': 'Rs', // 斯里兰卡卢比
'LT': '€', // 欧元
'LU': '€', // 欧元
'LV': '€', // 欧元
'MA': 'د.م.', // 摩洛哥迪拉姆
'MX': 'Mex$', // 墨西哥比索
'MY': 'RM', // 马来西亚林吉特
'NG': '₦', // 尼日利亚奈拉
'NL': '€', // 欧元
'NO': 'kr', // 挪威克朗
'NP': 'रू', // 尼泊尔卢比
'NZ': 'NZ$', // 新西兰元
'PK': '₨', // 巴基斯坦卢比
'PA': 'B/.', // 巴拿马巴波亚
'PE': 'S/.', // 秘鲁索尔
'PH': '₱', // 菲律宾比索
'PL': 'zł', // 波兰兹罗提
'PT': '€', // 欧元
'QA': 'ر.ق', // 卡塔尔里亚尔
'RO': 'lei', // 罗马尼亚列伊
'RU': '₽', // 俄罗斯卢布
'SA': 'ر.س', // 沙特里亚尔
'SG': 'S$', // 新加坡元
'SK': '€', // 欧元
'SI': '€', // 欧元
'SE': 'kr', // 瑞典克朗
'TH': '฿', // 泰铢
'TR': '₺', // 土耳其里拉
'TW': 'NT$', // 新台币
'UA': '₴', // 乌克兰格里夫纳
'US': '$', // 美元
'UY': '$U', // 乌拉圭比索
'VN': '₫', // 越南盾
'ZA': 'R', // 南非兰特
// 默认其他国家使用美元符号
};
// 定义历史记录项的接口
interface HistoryItem {
id: string;
timestamp: number;
value: string;
assessment: string;
assessmentColor: string;
salary: string;
countryCode: string;
countryName: string;
// 添加所有需要在分享页面展示的字段
cityFactor: string;
workHours: string;
commuteHours: string;
restTime: string;
dailySalary: string;
workDaysPerYear: string;
workDaysPerWeek: string;
wfhDaysPerWeek: string;
annualLeave: string;
paidSickLeave: string;
publicHolidays: string;
workEnvironment: string;
leadership: string;
teamwork: string;
degreeType: string;
schoolType: string;
education: string;
homeTown: string;
shuttle: string;
canteen: string;
workYears: string;
jobStability: string;
bachelorType: string;
hasShuttle: boolean;
hasCanteen: boolean;
}
// 定义表单数据接口
interface FormData {
salary: string;
nonChinaSalary: boolean;
workDaysPerWeek: string;
wfhDaysPerWeek: string;
annualLeave: string;
paidSickLeave: string;
publicHolidays: string;
workHours: string;
commuteHours: string;
restTime: string;
cityFactor: string;
workEnvironment: string;
leadership: string;
teamwork: string;
homeTown: string;
degreeType: string;
schoolType: string;
bachelorType: string;
workYears: string;
shuttle: string;
canteen: string;
jobStability: string;
education: string;
hasShuttle: boolean;
hasCanteen: boolean;
}
// 定义计算结果接口
interface Result {
value: number;
workDaysPerYear: number;
dailySalary: number;
assessment: string;
assessmentColor: string;
}
const SalaryCalculator = () => {
// 获取语言上下文
const { t, language } = useLanguage();
// 添加客户端检测
const [isBrowser, setIsBrowser] = useState(false);
// 添加滚动位置保存的引用
const scrollPositionRef = useRef(0);
// 添加历史记录状态
const [history, setHistory] = useState<HistoryItem[]>([]);
const [showHistory, setShowHistory] = useState(false);
// 在组件挂载时标记为浏览器环境
useEffect(() => {
setIsBrowser(true);
// 在客户端环境中执行重定向
if (typeof window !== 'undefined') {
const hostname = window.location.hostname;
if (hostname !== 'worthjob.zippland.com' && hostname !== 'localhost' && !hostname.includes('127.0.0.1')) {
window.location.href = 'https://worthjob.zippland.com' + window.location.pathname;
}
}
}, []);
// 添加用于创建分享图片的引用
const shareResultsRef = useRef<HTMLDivElement>(null);
// 状态管理 - 基础表单和选项
const [formData, setFormData] = useState<FormData>({
salary: '',
nonChinaSalary: false,
workDaysPerWeek: '5',
wfhDaysPerWeek: '0',
annualLeave: '5',
paidSickLeave: '3',
publicHolidays: '13',
workHours: '10',
commuteHours: '2',
restTime: '2',
cityFactor: '1.0',
workEnvironment: '1.0',
leadership: '1.0',
teamwork: '1.0',
homeTown: 'no',
degreeType: 'bachelor',
schoolType: 'firstTier',
bachelorType: 'firstTier',
workYears: '0',
shuttle: '1.0',
canteen: '1.0',
jobStability: 'private', // 新增:工作稳定度/类型
education: '1.0',
hasShuttle: false, // 确保这是一个明确的布尔值
hasCanteen: false, // 确保这是一个明确的布尔值
});
const [showPPPInput, setShowPPPInput] = useState(false);
// 修改为国家代码,默认为中国
const [selectedCountry, setSelectedCountry] = useState<string>('CN');
// 初始化时从localStorage加载国家设置
useEffect(() => {
// 从本地存储读取国家设置
if (typeof window !== 'undefined') {
const savedCountry = localStorage.getItem('selectedCountry');
if (savedCountry) {
setSelectedCountry(savedCountry);
}
}
}, []);
// 当国家选择改变时保存到localStorage
const handleCountryChange = (countryCode: string) => {
setSelectedCountry(countryCode);
if (typeof window !== 'undefined') {
localStorage.setItem('selectedCountry', countryCode);
}
};
const [result, setResult] = useState<Result | null>(null);
const [showPPPList, setShowPPPList] = useState(false);
const [assessment, setAssessment] = useState("");
const [assessmentColor, setAssessmentColor] = useState("text-gray-500");
const [visitorVisible, setVisitorVisible] = useState(false);
// 添加检查document对象存在的逻辑
useEffect(() => {
// 确保在客户端环境中执行
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const savedHistory = localStorage.getItem('jobValueHistory');
if (savedHistory) {
try {
const parsedHistory = JSON.parse(savedHistory) as Partial<HistoryItem>[];
// 处理历史记录,为可能缺失的字段添加默认值
const normalizedHistory: HistoryItem[] = parsedHistory.map((item: Partial<HistoryItem>) => ({
id: item.id || Date.now().toString(),
timestamp: item.timestamp || Date.now(),
value: item.value || '0',
assessment: item.assessment || 'rating_enter_salary',
assessmentColor: item.assessmentColor || 'text-gray-500',
salary: item.salary || '',
countryCode: item.countryCode || 'CN',
countryName: item.countryName || '中国',
// 为缺失的分享页面字段添加默认值
cityFactor: item.cityFactor || formData.cityFactor,
workHours: item.workHours || formData.workHours,
commuteHours: item.commuteHours || formData.commuteHours,
restTime: item.restTime || formData.restTime,
dailySalary: item.dailySalary || '0', // 简化,不使用函数
workDaysPerYear: item.workDaysPerYear || '250', // 简化,使用默认值
workDaysPerWeek: item.workDaysPerWeek || formData.workDaysPerWeek,
wfhDaysPerWeek: item.wfhDaysPerWeek || formData.wfhDaysPerWeek,
annualLeave: item.annualLeave || formData.annualLeave,
paidSickLeave: item.paidSickLeave || formData.paidSickLeave,
publicHolidays: item.publicHolidays || formData.publicHolidays,
workEnvironment: item.workEnvironment || formData.workEnvironment,
leadership: item.leadership || formData.leadership,
teamwork: item.teamwork || formData.teamwork,
degreeType: item.degreeType || formData.degreeType,
schoolType: item.schoolType || formData.schoolType,
education: item.education || formData.education,
homeTown: item.homeTown || formData.homeTown,
shuttle: item.shuttle || formData.shuttle,
canteen: item.canteen || formData.canteen,
workYears: item.workYears || formData.workYears,
jobStability: item.jobStability || formData.jobStability,
bachelorType: item.bachelorType || formData.bachelorType,
// 确保 hasShuttle 和 hasCanteen 有合法的布尔值,即使历史记录中没有这些字段
hasShuttle: typeof item.hasShuttle === 'boolean' ? item.hasShuttle : false,
hasCanteen: typeof item.hasCanteen === 'boolean' ? item.hasCanteen : false,
}));
setHistory(normalizedHistory);
} catch (e) {
console.error('加载历史记录失败', e);
}
}
}
}, [formData]);
// 监听访客统计加载
useEffect(() => {
// 延迟检查busuanzi是否已加载
const timer = setTimeout(() => {
const pv = document.getElementById('busuanzi_value_site_pv');
const uv = document.getElementById('busuanzi_value_site_uv');
if (pv && pv.innerText !== '') {
// 直接在现有数字上加上1700000原seeyoufarm统计数据
const currentCount = parseInt(pv.innerText, 10) || 0;
pv.innerText = (currentCount + 1700000).toString();
// 同时增加访客数的历史数据
if (uv && uv.innerText !== '') {
const currentUV = parseInt(uv.innerText, 10) || 0;
uv.innerText = (currentUV + 250000).toString();
}
setVisitorVisible(true);
} else {
// 如果未加载,再次尝试
const retryTimer = setTimeout(() => {
const pv = document.getElementById('busuanzi_value_site_pv');
const uv = document.getElementById('busuanzi_value_site_uv');
if (pv && pv.innerText !== '') {
// 直接在现有数字上加上1700000原seeyoufarm统计数据
const currentCount = parseInt(pv.innerText, 10) || 0;
pv.innerText = (currentCount + 1700000).toString();
// 同时增加访客数的历史数据
if (uv && uv.innerText !== '') {
const currentUV = parseInt(uv.innerText, 10) || 0;
uv.innerText = (currentUV + 1300000).toString();
}
setVisitorVisible(true);
}
}, 2000);
return () => clearTimeout(retryTimer);
}
}, 1000);
return () => clearTimeout(timer);
}, []);
// 添加滚动位置保存和恢复逻辑
useEffect(() => {
const handleBeforeStateChange = () => {
// 保存当前滚动位置
if (typeof window !== 'undefined') {
scrollPositionRef.current = window.scrollY;
}
};
const handleAfterStateChange = () => {
// 恢复滚动位置
if (typeof window !== 'undefined') {
setTimeout(() => {
window.scrollTo(0, scrollPositionRef.current);
}, 0);
}
};
// 添加到全局事件
window.addEventListener('beforeStateChange', handleBeforeStateChange);
window.addEventListener('afterStateChange', handleAfterStateChange);
return () => {
// 清理事件监听器
window.removeEventListener('beforeStateChange', handleBeforeStateChange);
window.removeEventListener('afterStateChange', handleAfterStateChange);
};
}, []);
const calculateWorkingDays = useCallback(() => {
const weeksPerYear = 52;
const totalWorkDays = weeksPerYear * Number(formData.workDaysPerWeek); // 确保转换为数字
const totalLeaves = Number(formData.annualLeave) + Number(formData.publicHolidays) + Number(formData.paidSickLeave) * 0.6; // 带薪病假按70%权重计算
return Math.max(totalWorkDays - totalLeaves, 0);
}, [formData.workDaysPerWeek, formData.annualLeave, formData.publicHolidays, formData.paidSickLeave]);
const calculateDailySalary = useCallback(() => {
if (!formData.salary) return 0;
const workingDays = calculateWorkingDays();
// 应用PPP转换因子标准化薪资
// 如果选择了非中国地区使用选定国家的PPP否则使用中国默认值4.19
const isNonChina = selectedCountry !== 'CN';
const pppFactor = isNonChina ? pppFactors[selectedCountry] || 4.19 : 4.19;
const standardizedSalary = Number(formData.salary) * (4.19 / pppFactor);
return standardizedSalary / workingDays; // 除 0 不管, Infinity(爽到爆炸)!
}, [formData.salary, selectedCountry, calculateWorkingDays]);
// 新增:获取显示用的日薪(转回原始货币)
const getDisplaySalary = useCallback(() => {
const dailySalaryInCNY = calculateDailySalary();
const isNonChina = selectedCountry !== 'CN';
if (isNonChina) {
// 非中国地区,转回本地货币
const pppFactor = pppFactors[selectedCountry] || 4.19;
return (dailySalaryInCNY * pppFactor / 4.19).toFixed(2);
} else {
return dailySalaryInCNY.toFixed(2);
}
}, [calculateDailySalary, selectedCountry]);
const handleInputChange = useCallback((name: string, value: string | boolean) => {
// 触发自定义事件,保存滚动位置
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('beforeStateChange'));
}
// 直接设置值,不进行任何验证
setFormData(prev => ({
...prev,
[name]: value
}));
// 在状态更新后,触发恢复滚动位置事件
setTimeout(() => {
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('afterStateChange'));
}
}, 0);
}, []);
const calculateValue = () => {
if (!formData.salary) return 0;
const dailySalary = calculateDailySalary();
const workHours = Number(formData.workHours);
const commuteHours = Number(formData.commuteHours);
const restTime = Number(formData.restTime);
// 确保正确转换为数字使用parseFloat可以更可靠地处理字符串转数字
const workDaysPerWeek = parseFloat(formData.workDaysPerWeek) || 5;
// 允许wfhDaysPerWeek为空字符串计算时才处理为0
const wfhInput = formData.wfhDaysPerWeek.trim();
const wfhDaysPerWeek = wfhInput === '' ? 0 : Math.min(parseFloat(wfhInput) || 0, workDaysPerWeek);
// 确保有办公室工作天数时才计算比例否则设为0
const officeDaysRatio = workDaysPerWeek > 0 ? (workDaysPerWeek - wfhDaysPerWeek) / workDaysPerWeek : 0;
// 在计算结果中添加一个小的日志输出,以便调试
console.log('WFH计算:', {
workDaysPerWeek,
wfhDaysPerWeek,
officeDaysRatio,
effectiveCommute: commuteHours * officeDaysRatio
});
// 班车系数只在勾选时使用否则为1.0
const shuttleFactor = formData.hasShuttle ? Number(formData.shuttle) : 1.0;
const effectiveCommuteHours = commuteHours * officeDaysRatio * shuttleFactor;
// 食堂系数只在勾选时使用否则为1.0
const canteenFactor = formData.hasCanteen ? Number(formData.canteen) : 1.0;
// 工作环境因素,包含食堂和家乡因素
const environmentFactor = Number(formData.workEnvironment) *
Number(formData.leadership) *
Number(formData.teamwork) *
Number(formData.cityFactor) *
canteenFactor;
// 根据工作年限计算经验薪资倍数
const workYears = Number(formData.workYears);
let experienceSalaryMultiplier = 1.0;
if (workYears === 0) {
// 应届生:直接根据工作类型设定初始调整系数,反映稳定性/风险价值
// 注意:这些系数在分母中,系数越小,最终价值越高
if (formData.jobStability === 'government') {
experienceSalaryMultiplier = 0.8; // 体制内稳定性高,价值相对高
} else if (formData.jobStability === 'state') {
experienceSalaryMultiplier = 0.9; // 央/国企较稳定,价值相对高
} else if (formData.jobStability === 'foreign') {
experienceSalaryMultiplier = 0.95; // 外企,较为稳定
} else if (formData.jobStability === 'private') {
experienceSalaryMultiplier = 1.0; // 私企作为基准
} else if (formData.jobStability === 'dispatch') {
experienceSalaryMultiplier = 1.1; // 派遣社员风险高,价值相对低
} else if (formData.jobStability === 'freelance') {
experienceSalaryMultiplier = 1.1; // 自由职业风险高,价值相对低
}
} else {
// 非应届生:使用基于增长预期的模型
// 基准薪资增长曲线(适用于私企)
let baseSalaryMultiplier = 1.0;
if (workYears === 1) baseSalaryMultiplier = 1.5; // 1年1.50-2.00,取中间值
else if (workYears <= 3) baseSalaryMultiplier = 2.2; // 2-3年2.20-2.50,取中间值
else if (workYears <= 5) baseSalaryMultiplier = 2.7; // 4-5年2.70-3.00,取中间值
else if (workYears <= 8) baseSalaryMultiplier = 3.2; // 6-8年3.20-3.50,取中间值
else if (workYears <= 10) baseSalaryMultiplier = 3.6; // 9-10年3.60-3.80,取中间值
else baseSalaryMultiplier = 3.9; // 11-13年3.90-4.20,取中间值
// 工作单位类型对涨薪幅度的影响系数
let salaryGrowthFactor = 1.0; // 私企基准
if (formData.jobStability === 'foreign') {
salaryGrowthFactor = 0.8; // 外企涨薪幅度为私企的80%
} else if (formData.jobStability === 'state') {
salaryGrowthFactor = 0.4; // 央/国企涨薪幅度为私企的40%
} else if (formData.jobStability === 'government') {
salaryGrowthFactor = 0.2; // 体制内涨薪幅度为私企的20%
} else if (formData.jobStability === 'dispatch') {
salaryGrowthFactor = 1.2; // 派遣社员涨薪幅度为私企的120%(体现不稳定性)
} else if (formData.jobStability === 'freelance') {
salaryGrowthFactor = 1.2; // 自由职业涨薪幅度为私企的120%(体现不稳定性)
}
// 根据公式: 1 + (对应幅度-1) * 工作单位系数,计算最终薪资倍数
experienceSalaryMultiplier = 1 + (baseSalaryMultiplier - 1) * salaryGrowthFactor;
}
// 薪资满意度应该受到经验薪资倍数的影响
// 相同薪资,对于高经验者来说价值更低,对应的计算公式需要考虑经验倍数
return (dailySalary * environmentFactor) /
(35 * (workHours + effectiveCommuteHours - 0.5 * restTime) * Number(formData.education) * experienceSalaryMultiplier);
};
const value = calculateValue();
const getValueAssessment = useCallback(() => {
if (!formData.salary) return { text: t('rating_enter_salary'), color: "text-gray-500" };
if (value < 0.6) return { text: t('rating_terrible'), color: "text-pink-800" };
if (value < 1.0) return { text: t('rating_poor'), color: "text-red-500" };
if (value <= 1.8) return { text: t('rating_average'), color: "text-orange-500" };
if (value <= 2.5) return { text: t('rating_good'), color: "text-blue-500" };
if (value <= 3.2) return { text: t('rating_great'), color: "text-green-500" };
if (value <= 4.0) return { text: t('rating_excellent'), color: "text-purple-500" };
return { text: t('rating_perfect'), color: "text-yellow-400" };
}, [formData.salary, value, t]);
// 获取评级的翻译键,用于分享链接
const getValueAssessmentKey = useCallback(() => {
if (!formData.salary) return 'rating_enter_salary';
if (value < 0.6) return 'rating_terrible';
if (value < 1.0) return 'rating_poor';
if (value <= 1.8) return 'rating_average';
if (value <= 2.5) return 'rating_good';
if (value <= 3.2) return 'rating_great';
if (value <= 4.0) return 'rating_excellent';
return 'rating_perfect';
}, [formData.salary, value]);
const RadioGroup = ({ label, name, value, onChange, options }: {
label: string;
name: string;
value: string;
onChange: (name: string, value: string | boolean) => void;
options: Array<{ label: string; value: string; }>;
}) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{label}</label>
<div className={`grid ${language === 'en' ? 'grid-cols-3' : 'grid-cols-4'} gap-2`}>
{options.map((option) => (
<button
key={option.value}
className={`px-3 py-2 rounded-md text-sm transition-colors
${value === option.value
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 font-medium'
: 'bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-300'}`}
onClick={(e) => {
e.preventDefault(); // 阻止默认行为
e.stopPropagation(); // 阻止事件冒泡
onChange(name, option.value);
}}
type="button"
>
{option.label}
</button>
))}
</div>
</div>
);
// 根据学位类型和学校类型计算教育系数
const calculateEducationFactor = useCallback(() => {
const degreeType = formData.degreeType;
const schoolType = formData.schoolType;
const bachelorType = formData.bachelorType;
// 使用更简单的方式计算系数,避免复杂的索引类型问题
let factor = 1.0; // 默认值
// 专科及以下固定为0.8
if (degreeType === 'belowBachelor') {
factor = 0.8;
}
// 本科学历
else if (degreeType === 'bachelor') {
if (schoolType === 'secondTier') factor = 0.9; // 二本三本
else if (schoolType === 'firstTier') factor = 1.0; // 双非/QS100/USnews50
else if (schoolType === 'elite') factor = 1.2; // 985/211/QS30/USnews20
}
// 硕士学历 - 考虑本科背景
else if (degreeType === 'masters') {
// 先获取本科背景的基础系数
let bachelorBaseCoefficient = 0;
if (bachelorType === 'secondTier') bachelorBaseCoefficient = 0.9; // 二本三本
else if (bachelorType === 'firstTier') bachelorBaseCoefficient = 1.0; // 双非/QS100/USnews50
else if (bachelorType === 'elite') bachelorBaseCoefficient = 1.2; // 985/211/QS30/USnews20
// 再计算硕士学校的加成系数
let mastersBonus = 0;
if (schoolType === 'secondTier') mastersBonus = 0.4; // 二本三本硕士
else if (schoolType === 'firstTier') mastersBonus = 0.5; // 双非/QS100/USnews50硕士
else if (schoolType === 'elite') mastersBonus = 0.6; // 985/211/QS30/USnews20硕士
// 最终学历系数 = 本科基础 + 硕士加成
factor = bachelorBaseCoefficient + mastersBonus;
}
// 博士学历
else if (degreeType === 'phd') {
if (schoolType === 'secondTier') factor = 1.6; // 二本三本博士
else if (schoolType === 'firstTier') factor = 1.8; // 双非/QS100/USnews50博士
else if (schoolType === 'elite') factor = 2.0; // 985/211/QS30/USnews20博士
}
// 更新education字段
if (formData.education !== String(factor)) {
// 这里不使用handleInputChange以避免触发滚动保存/恢复逻辑
setFormData(prev => ({
...prev,
education: String(factor)
}));
}
return factor;
}, [formData.degreeType, formData.schoolType, formData.bachelorType, formData.education]);
// 在组件初始化和学历选择变化时计算教育系数
useEffect(() => {
calculateEducationFactor();
}, [formData.degreeType, formData.schoolType, calculateEducationFactor]);
// 获取当前选择的国家名称(根据语言)
const getCountryName = useCallback((countryCode: string) => {
if (language === 'en') {
return countryNames.en[countryCode] || countryCode || 'Unknown';
}
if (language === 'ja') {
return countryNames.ja[countryCode] || countryCode || '不明';
}
return countryNames.zh[countryCode] || countryCode || '未知';
}, [language]);
// 保存当前记录到历史中
const saveToHistory = useCallback(() => {
if (!formData.salary || typeof window === 'undefined') return;
const newHistoryItem: HistoryItem = {
id: Date.now().toString(),
timestamp: Date.now(),
value: value.toFixed(2),
assessment: getValueAssessmentKey(), // 使用翻译键而不是已翻译的文本
assessmentColor: getValueAssessment().color,
salary: formData.salary,
countryCode: selectedCountry,
countryName: getCountryName(selectedCountry),
// 添加所有需要在分享页面展示的字段
cityFactor: formData.cityFactor,
workHours: formData.workHours,
commuteHours: formData.commuteHours,
restTime: formData.restTime,
dailySalary: getDisplaySalary(),
workDaysPerYear: calculateWorkingDays().toString(),
workDaysPerWeek: formData.workDaysPerWeek,
wfhDaysPerWeek: formData.wfhDaysPerWeek,
annualLeave: formData.annualLeave,
paidSickLeave: formData.paidSickLeave,
publicHolidays: formData.publicHolidays,
workEnvironment: formData.workEnvironment,
leadership: formData.leadership,
teamwork: formData.teamwork,
degreeType: formData.degreeType,
schoolType: formData.schoolType,
education: formData.education,
homeTown: formData.homeTown,
shuttle: formData.hasShuttle ? formData.shuttle : '1.0',
canteen: formData.hasCanteen ? formData.canteen : '1.0',
workYears: formData.workYears,
jobStability: formData.jobStability,
bachelorType: formData.bachelorType,
hasShuttle: formData.hasShuttle,
hasCanteen: formData.hasCanteen,
};
try {
const updatedHistory = [newHistoryItem, ...history.slice(0, 9)]; // 限制保存10条记录
setHistory(updatedHistory);
localStorage.setItem('jobValueHistory', JSON.stringify(updatedHistory));
console.log('保存历史记录成功', newHistoryItem);
} catch (e) {
console.error('保存历史记录失败', e);
}
return newHistoryItem;
}, [formData, value, getValueAssessmentKey, getValueAssessment, selectedCountry, history, getCountryName, calculateWorkingDays, getDisplaySalary, formData.hasShuttle, formData.hasCanteen]);
// 删除单条历史记录
const deleteHistoryItem = useCallback((id: string, e: React.MouseEvent) => {
e.stopPropagation(); // 阻止事件冒泡
e.preventDefault(); // 阻止默认行为
try {
const updatedHistory = history.filter(item => item.id !== id);
setHistory(updatedHistory);
localStorage.setItem('jobValueHistory', JSON.stringify(updatedHistory));
console.log('删除历史记录成功', id);
} catch (e) {
console.error('删除历史记录失败', e);
}
}, [history]);
// 清空所有历史记录
const clearAllHistory = useCallback((e: React.MouseEvent) => {
e.stopPropagation(); // 阻止事件冒泡
e.preventDefault(); // 阻止默认行为
try {
setHistory([]);
localStorage.removeItem('jobValueHistory');
console.log('清空所有历史记录成功');
} catch (e) {
console.error('清空历史记录失败', e);
}
}, []);
// 格式化日期
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
};
// 获取当前选择国家的货币符号
const getCurrencySymbol = useCallback((countryCode: string) => {
return currencySymbols[countryCode] || '$'; // 如果没有找到对应货币符号,默认使用美元符号
}, []);
return (
<div className="max-w-2xl mx-auto p-4 sm:p-6">
<div className="mb-4 text-center">
<h1 className="text-3xl md:text-4xl font-bold mb-2 bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-purple-600 py-2">{t('title')}</h1>
<div className="mb-3">
<a
href="https://github.com/zippland/worth-calculator"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400 transition-colors inline-flex items-center gap-1.5"
>
<Star className="h-3.5 w-3.5" />
{t('star_request')}
</a>
</div>
<div className="flex items-center justify-center gap-3 mb-2">
<p className="text-sm text-gray-500 dark:text-gray-400">v6.2.1</p>
<a
href="https://github.com/zippland/worth-calculator"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-500 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 transition-colors flex items-center gap-1"
>
<Github className="h-3.5 w-3.5" />
{t('github')}
</a>
<a
href="https://www.xiaohongshu.com/user/profile/623e8b080000000010007721?xsec_token=YBzoLUB4HsSITTBOgPAXY-0Gvqvn3HqHpcDeA3sHhDh-M%3D&xsec_source=app_share&xhsshare=CopyLink&appuid=5c5d5259000000001d00ef04&apptime=1743400694&share_id=b9bfcd5090f9473daf5c1d1dc3eb0921&share_channel=copy_link"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-500 hover:text-pink-500 dark:text-gray-400 dark:hover:text-pink-400 transition-colors flex items-center gap-1"
>
<Book className="h-3.5 w-3.5" />
{t('xiaohongshu')}
</a>
{/* 仅在客户端渲染历史记录按钮 */}
{isBrowser && (
<button
onClick={() => setShowHistory(!showHistory)}
className="text-sm text-gray-500 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 transition-colors flex items-center gap-1 cursor-pointer"
>
<History className="h-3.5 w-3.5" />
{t('history')}
</button>
)}
</div>
{/* 历史记录列表 - 仅在客户端渲染 */}
{isBrowser && showHistory && (
<div className="relative z-10">
<div className="absolute left-1/2 transform -translate-x-1/2 mt-1 w-72 md:w-96 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 max-h-80 overflow-y-auto">
<div className="p-3">
<div className="flex justify-between items-center mb-3 border-b pb-2 border-gray-200 dark:border-gray-700">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 flex items-center">
<History className="h-3.5 w-3.5 mr-1" />
{t('history')}
</h3>
<div className="flex gap-2">
{history.length > 0 && (
<button
onClick={clearAllHistory}
className="text-xs text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-colors px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
>
{t('clear_all')}
</button>
)}
<button
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
setShowHistory(false);
}}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 w-6 h-6 flex items-center justify-center rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
>
×
</button>
</div>
</div>
{history.length > 0 ? (
<ul className="space-y-2">
{history.map((item) => (
<li key={item.id} className="flex items-center justify-between p-2 rounded-lg bg-gray-50 dark:bg-gray-750 hover:bg-blue-50 dark:hover:bg-gray-700 transition-colors border border-gray-100 dark:border-gray-600">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`text-sm font-semibold ${item.assessmentColor}`}>{item.value}</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300">
{item.countryCode !== 'CN' ? '$' : '¥'}{item.salary}
</span>
</div>
<div className="text-xs text-gray-500 flex items-center">
<span>{formatDate(item.timestamp)}</span>
</div>
</div>
<div className="flex gap-1">
<button
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
e.preventDefault(); // 阻止默认行为
// 恢复历史记录中的值到当前表单
setFormData({
...formData,
salary: item.salary,
cityFactor: item.cityFactor,
workHours: item.workHours,
commuteHours: item.commuteHours,
restTime: item.restTime,
workDaysPerWeek: item.workDaysPerWeek,
wfhDaysPerWeek: item.wfhDaysPerWeek,
annualLeave: item.annualLeave,
paidSickLeave: item.paidSickLeave,
publicHolidays: item.publicHolidays,
workEnvironment: item.workEnvironment,
leadership: item.leadership,
teamwork: item.teamwork,
degreeType: item.degreeType,
schoolType: item.schoolType,
education: item.education,
homeTown: item.homeTown,
shuttle: item.shuttle,
canteen: item.canteen,
workYears: item.workYears,
jobStability: item.jobStability,
bachelorType: item.bachelorType,
// 确保 hasShuttle 和 hasCanteen 有合法的布尔值
hasShuttle: typeof item.hasShuttle === 'boolean' ? item.hasShuttle : false,
hasCanteen: typeof item.hasCanteen === 'boolean' ? item.hasCanteen : false,
});
// 设置国家
handleCountryChange(item.countryCode);
// 关闭历史记录面板
setShowHistory(false);
}}
className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 p-1.5 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
title={t('restore_history')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<Link
href={{
pathname: '/share',
query: {
value: item.value,
assessment: item.assessment, // 传递翻译键而不是文本
assessmentColor: item.assessmentColor,
cityFactor: item.cityFactor,
workHours: item.workHours,
commuteHours: item.commuteHours,
restTime: item.restTime,
dailySalary: item.dailySalary,
isYuan: item.countryCode !== 'CN' ? 'false' : 'true',
workDaysPerYear: item.workDaysPerYear,
workDaysPerWeek: item.workDaysPerWeek,
wfhDaysPerWeek: item.wfhDaysPerWeek,
annualLeave: item.annualLeave,
paidSickLeave: item.paidSickLeave,
publicHolidays: item.publicHolidays,
workEnvironment: item.workEnvironment,
leadership: item.leadership,
teamwork: item.teamwork,
degreeType: item.degreeType,
schoolType: item.schoolType,
education: item.education,
homeTown: item.homeTown,
shuttle: item.shuttle,
canteen: item.canteen,
workYears: item.workYears,
jobStability: item.jobStability,
bachelorType: item.bachelorType,
countryCode: item.countryCode,
countryName: getCountryName(item.countryCode),
hasShuttle: item.hasShuttle,
hasCanteen: item.hasCanteen,
}
}}
className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 p-1.5 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
>
<Eye className="w-4 h-4" />
</Link>
<button
onClick={(e) => deleteHistoryItem(item.id, e)}
className="text-gray-400 hover:text-red-500 dark:hover:text-red-400 p-1.5 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
title={t('delete_history')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</li>
))}
</ul>
) : (
<div className="text-center py-8 px-4">
<div className="text-gray-400 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 mx-auto opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('no_history')}
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
{t('history_notice')}
</p>
</div>
)}
</div>
</div>
</div>
)}
<div className="flex justify-center mb-2">
<LanguageSwitcher />
</div>
{/* 访问统计 - 仅在客户端渲染 */}
{isBrowser && (
<div className="mt-1 text-xs text-gray-400 dark:text-gray-600 flex justify-center gap-4">
<span id="busuanzi_container_site_pv" className={`transition-opacity duration-300 ${visitorVisible ? 'opacity-100' : 'opacity-0'}`}>
{t('visits')}: <span id="busuanzi_value_site_pv"></span>
</span>
<span id="busuanzi_container_site_uv" className={`transition-opacity duration-300 ${visitorVisible ? 'opacity-100' : 'opacity-0'}`}>
{t('visitors')}: <span id="busuanzi_value_site_uv"></span>
</span>
</div>
)}
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl shadow-gray-200/50 dark:shadow-black/30">
<div className="p-6 space-y-8">
{/* 薪资与工作时间 section */}
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{selectedCountry !== 'CN' ?
`${t('annual_salary')}(${getCurrencySymbol(selectedCountry)})` :
t('annual_salary_cny')}
</label>
<div className="flex items-center gap-2 mt-1">
<Wallet className="w-4 h-4 text-gray-500 dark:text-gray-400" />
<input
type="number"
value={formData.salary}
onChange={(e) => handleInputChange('salary', e.target.value)}
placeholder={selectedCountry !== 'CN' ?
`${t('salary_placeholder')} ${getCurrencySymbol(selectedCountry)}` :
t('salary_placeholder_cny')}
className="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="relative">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('country_selection')}
<span className="ml-1 inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300 cursor-pointer group relative">
?
<span className="absolute z-10 invisible group-hover:visible bg-gray-900 text-white text-xs rounded py-1 px-2 bottom-full mb-1 left-1/2 transform -translate-x-1/2 w-48 sm:w-64">
{t('ppp_tooltip')}
</span>
</span>
</label>
<select
id="country"
name="country"
value={selectedCountry}
onChange={(e) => handleCountryChange(e.target.value)}
className="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
{Object.keys(pppFactors).sort((a, b) => {
// 确保中国始终排在第一位
if (a === 'CN') return -1;
if (b === 'CN') return 1;
return new Intl.Collator(['zh', 'ja', 'en']).compare(getCountryName(a), getCountryName(b));
}).map(code => (
<option key={code} value={code}>
{getCountryName(code)} ({pppFactors[code].toFixed(2)})
</option>
))}
</select>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t('selected_ppp')}: {(pppFactors[selectedCountry] || 4.19).toFixed(2)}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('work_days_per_week')}</label>
<input
type="number"
value={formData.workDaysPerWeek}
onChange={(e) => handleInputChange('workDaysPerWeek', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('wfh_days_per_week')}
<span className="ml-1 inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300 cursor-pointer group relative">
?
<span className="absolute z-10 invisible group-hover:visible bg-gray-900 text-white text-xs rounded py-1 px-2 bottom-full mb-1 left-1/2 transform -translate-x-1/2 w-48 sm:w-64">
{t('wfh_tooltip')}
</span>
</span>
</label>
<input
type="number"
min="0"
max={formData.workDaysPerWeek}
step="1"
value={formData.wfhDaysPerWeek}
onChange={(e) => handleInputChange('wfhDaysPerWeek', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('annual_leave')}</label>
<input
type="number"
value={formData.annualLeave}
onChange={(e) => handleInputChange('annualLeave', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('public_holidays')}</label>
<input
type="number"
value={formData.publicHolidays}
onChange={(e) => handleInputChange('publicHolidays', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('paid_sick_leave')}</label>
<input
type="number"
value={formData.paidSickLeave}
onChange={(e) => handleInputChange('paidSickLeave', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('work_hours')}
<span className="ml-1 inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300 cursor-pointer group relative">
?
<span className="absolute z-10 invisible group-hover:visible bg-gray-900 text-white text-xs rounded py-1 px-2 bottom-full mb-1 left-1/2 transform -translate-x-1/2 w-48 sm:w-64">
{t('work_hours_tooltip')}
</span>
</span>
</label>
<input
type="number"
value={formData.workHours}
onChange={(e) => handleInputChange('workHours', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('commute_hours')}
<span className="ml-1 inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300 cursor-pointer group relative">
?
<span className="absolute z-10 invisible group-hover:visible bg-gray-900 text-white text-xs rounded py-1 px-2 bottom-full mb-1 left-1/2 transform -translate-x-1/2 w-48 sm:w-64">
{t('commute_tooltip')}
</span>
</span>
</label>
<input
type="number"
value={formData.commuteHours}
onChange={(e) => handleInputChange('commuteHours', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('rest_time')}</label>
<input
type="number"
value={formData.restTime}
onChange={(e) => handleInputChange('restTime', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
/>
</div>
</div>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 my-6"></div>
{/* 环境系数 */}
<div className="space-y-4">
{/* 学历和工作年限 */}
<div className="space-y-4">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('education_level')}</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{t('degree_type')}</label>
<select
value={formData.degreeType}
onChange={(e) => handleInputChange('degreeType', e.target.value)}
className="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
>
<option value="belowBachelor">{t('below_bachelor')}</option>
<option value="bachelor">{t('bachelor')}</option>
<option value="masters">{t('masters')}</option>
<option value="phd">{t('phd')}</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{t('school_type')}</label>
<select
value={formData.schoolType}
onChange={(e) => handleInputChange('schoolType', e.target.value)}
className="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
disabled={formData.degreeType === 'belowBachelor'}
>
<option value="secondTier">{t('school_second_tier')}</option>
{formData.degreeType === 'bachelor' ? (
<>
<option value="firstTier">{t('school_first_tier_bachelor')}</option>
<option value="elite">{t('school_elite_bachelor')}</option>
</>
) : (
<>
<option value="firstTier">{t('school_first_tier_higher')}</option>
<option value="elite">{t('school_elite_higher')}</option>
</>
)}
</select>
</div>
</div>
{/* 硕士显示本科背景选项 */}
{formData.degreeType === 'masters' && (
<div className="mt-4">
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">{t('bachelor_background')}</label>
<select
value={formData.bachelorType}
onChange={(e) => handleInputChange('bachelorType', e.target.value)}
className="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
>
<option value="secondTier">{t('school_second_tier')}</option>
<option value="firstTier">{t('school_first_tier_bachelor')}</option>
<option value="elite">{t('school_elite_bachelor')}</option>
</select>
</div>
)}
</div>
{/* 工作年限选择 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('work_years')}</label>
<select
value={formData.workYears}
onChange={(e) => handleInputChange('workYears', e.target.value)}
className="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 dark:text-white"
>
<option value="0">{t('fresh_graduate')}</option>
<option value="1">{t('years_1_3')}</option>
<option value="2">{t('years_3_5')}</option>
<option value="4">{t('years_5_8')}</option>
<option value="6">{t('years_8_10')}</option>
<option value="10">{t('years_10_12')}</option>
<option value="15">{t('years_above_12')}</option>
</select>
</div>
</div>
{/* 添加工作类型RadioGroup */}
<RadioGroup
label={t('job_stability')}
name="jobStability"
value={formData.jobStability}
onChange={handleInputChange}
options={[
{ label: t('job_government'), value: 'government' },
{ label: t('job_state'), value: 'state' },
{ label: t('job_foreign'), value: 'foreign' },
{ label: t('job_private'), value: 'private' },
{ label: t('job_dispatch'), value: 'dispatch' },
{ label: t('job_freelance'), value: 'freelance' },
]}
/>
<RadioGroup
label={t('work_environment')}
name="workEnvironment"
value={formData.workEnvironment}
onChange={handleInputChange}
options={[
{ label: t('env_remote'), value: '0.8' },
{ label: t('env_factory'), value: '0.9' },
{ label: t('env_normal'), value: '1.0' },
{ label: t('env_cbd'), value: '1.1' },
]}
/>
<RadioGroup
label={t('city_factor')}
name="cityFactor"
value={formData.cityFactor}
onChange={handleInputChange}
options={[
{ label: t('city_tier1'), value: '0.70' },
{ label: t('city_newtier1'), value: '0.80' },
{ label: t('city_tier2'), value: '1.0' },
{ label: t('city_tier3'), value: '1.10' },
{ label: t('city_tier4'), value: '1.25' },
{ label: t('city_county'), value: '1.40' },
{ label: t('city_town'), value: '1.50' },
]}
/>
<RadioGroup
label={t('hometown')}
name="homeTown"
value={formData.homeTown}
onChange={handleInputChange}
options={[
{ label: t('not_hometown'), value: 'no' },
{ label: t('is_hometown'), value: 'yes' },
]}
/>
<RadioGroup
label={t('leadership')}
name="leadership"
value={formData.leadership}
onChange={handleInputChange}
options={[
{ label: t('leader_bad'), value: '0.7' },
{ label: t('leader_strict'), value: '0.9' },
{ label: t('leader_normal'), value: '1.0' },
{ label: t('leader_good'), value: '1.1' },
{ label: t('leader_favorite'), value: '1.3' },
]}
/>
<RadioGroup
label={t('teamwork')}
name="teamwork"
value={formData.teamwork}
onChange={handleInputChange}
options={[
{ label: t('team_bad'), value: '0.9' },
{ label: t('team_normal'), value: '1.0' },
{ label: t('team_good'), value: '1.1' },
{ label: t('team_excellent'), value: '1.2' },
]}
/>
{/* 班车和食堂选项作为加分项,加上勾选框控制 */}
<div className="space-y-2">
<div className="flex items-center mb-2">
<input
id="hasShuttle"
type="checkbox"
checked={formData.hasShuttle === true}
onChange={(e) => handleInputChange('hasShuttle', e.target.checked)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
<label htmlFor="hasShuttle" className="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{t('shuttle')}
</label>
</div>
{formData.hasShuttle && (
<RadioGroup
label=""
name="shuttle"
value={formData.shuttle}
onChange={handleInputChange}
options={[
{ label: t('shuttle_none'), value: '1.0' },
{ label: t('shuttle_inconvenient'), value: '0.9' },
{ label: t('shuttle_convenient'), value: '0.7' },
{ label: t('shuttle_direct'), value: '0.5' },
]}
/>
)}
</div>
<div className="space-y-2">
<div className="flex items-center mb-2">
<input
id="hasCanteen"
type="checkbox"
checked={formData.hasCanteen === true}
onChange={(e) => handleInputChange('hasCanteen', e.target.checked)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
/>
<label htmlFor="hasCanteen" className="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{t('canteen')}
</label>
</div>
{formData.hasCanteen && (
<RadioGroup
label=""
name="canteen"
value={formData.canteen}
onChange={handleInputChange}
options={[
{ label: t('canteen_none'), value: '1.0' },
{ label: t('canteen_average'), value: '1.05' },
{ label: t('canteen_good'), value: '1.1' },
{ label: t('canteen_excellent'), value: '1.15' },
]}
/>
)}
</div>
</div>
</div>
</div>
{/* 结果卡片优化 */}
<div ref={shareResultsRef} className="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-xl p-6 shadow-inner">
<div className="grid grid-cols-3 gap-8">
<div>
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">{t('working_days_per_year')}</div>
<div className="text-2xl font-semibold mt-1 text-gray-900 dark:text-white">{calculateWorkingDays()}{t('days_unit')}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">{t('average_daily_salary')}</div>
<div className="text-2xl font-semibold mt-1 text-gray-900 dark:text-white">
{getCurrencySymbol(selectedCountry)}{getDisplaySalary()}
</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">{t('job_value')}</div>
<div className={`text-2xl font-semibold mt-1 ${getValueAssessment().color}`}>
{value.toFixed(2)}
<span className="text-base ml-2">({getValueAssessment().text})</span>
</div>
</div>
</div>
{/* 修改分享按钮为链接到分享页面,并保存到历史 */}
<div className="mt-6 flex justify-end">
<Link
href={{
pathname: '/share',
query: {
value: value.toFixed(2),
assessment: getValueAssessmentKey(),
assessmentColor: getValueAssessment().color,
cityFactor: formData.cityFactor,
workHours: formData.workHours,
commuteHours: formData.commuteHours,
restTime: formData.restTime,
dailySalary: getDisplaySalary(),
isYuan: selectedCountry !== 'CN' ? 'false' : 'true',
workDaysPerYear: calculateWorkingDays().toString(),
workDaysPerWeek: formData.workDaysPerWeek,
wfhDaysPerWeek: formData.wfhDaysPerWeek,
annualLeave: formData.annualLeave,
paidSickLeave: formData.paidSickLeave,
publicHolidays: formData.publicHolidays,
workEnvironment: formData.workEnvironment,
leadership: formData.leadership,
teamwork: formData.teamwork,
degreeType: formData.degreeType,
schoolType: formData.schoolType,
education: formData.education,
homeTown: formData.homeTown,
shuttle: formData.hasShuttle ? formData.shuttle : '1.0',
canteen: formData.hasCanteen ? formData.canteen : '1.0',
workYears: formData.workYears,
jobStability: formData.jobStability,
bachelorType: formData.bachelorType,
countryCode: selectedCountry,
countryName: getCountryName(selectedCountry),
currencySymbol: getCurrencySymbol(selectedCountry),
hasShuttle: formData.hasShuttle,
hasCanteen: formData.hasCanteen,
}
}}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors
${formData.salary ? 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800' :
'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-600'}`}
onClick={() => formData.salary ? saveToHistory() : null}
>
<FileText className="w-4 h-4" />
{t('view_report')}
</Link>
</div>
</div>
</div>
);
};
export default SalaryCalculator;