first commit

This commit is contained in:
Zihan
2024-10-31 03:54:57 +11:00
parent de98fa7682
commit a8a24f9fbc
5 changed files with 289 additions and 102 deletions

BIN
README.md

Binary file not shown.

View File

@@ -1,101 +1,9 @@
import Image from "next/image";
import Calculator from '@/components/calculator';
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
<main className="min-h-screen bg-gray-50">
<Calculator />
</main>
);
}
}

268
components/calculator.tsx Normal file
View File

@@ -0,0 +1,268 @@
"use client";
import React, { useState, useCallback } from 'react';
import { Wallet, Clock, Calendar } from 'lucide-react';
const SalaryCalculator = () => {
const [formData, setFormData] = useState({
annualSalary: '', // 年薪
workDaysPerWeek: 5, // 每周工作天数
annualLeave: 5, // 年假天数
publicHolidays: 11, // 法定节假日
workHours: 8, // 工作时长
commuteHours: 1, // 通勤时长
breakHours: 1, // 预备时长
workEnvironment: '1.0', // 工作环境系数
heterogeneity: '1.0', // 异性环境系数
teamwork: '1.0', // 同事环境系数
education: '1.0' // 学历系数
});
const calculateWorkingDays = useCallback(() => {
const weeksPerYear = 52;
const totalWorkDays = weeksPerYear * formData.workDaysPerWeek;
const totalLeaves = Number(formData.annualLeave) + Number(formData.publicHolidays);
return totalWorkDays - totalLeaves;
}, [formData.workDaysPerWeek, formData.annualLeave, formData.publicHolidays]);
const calculateDailySalary = useCallback(() => {
if (!formData.annualSalary) return 0;
const workingDays = calculateWorkingDays();
return Number(formData.annualSalary) / workingDays;
}, [formData.annualSalary, calculateWorkingDays]);
const handleInputChange = (name: string, value: string) => {
setFormData(prev => ({
...prev,
[name]: value
}));
};
const calculateValue = () => {
if (!formData.annualSalary) return 0;
const dailySalary = calculateDailySalary();
const workHours = Number(formData.workHours);
const commuteHours = Number(formData.commuteHours);
const breakHours = Number(formData.breakHours);
const environmentFactor = Number(formData.workEnvironment) *
Number(formData.heterogeneity) *
Number(formData.teamwork);
return (dailySalary * environmentFactor) /
(35 * (workHours + commuteHours - 0.5 * breakHours) * Number(formData.education));
};
const value = calculateValue();
const getValueAssessment = () => {
if (!formData.annualSalary) return { text: "请输入年薪", color: "text-gray-500" };
if (value < 0.8) return { text: "很惨", color: "text-red-500" };
if (value <= 1.5) return { text: "一般", color: "text-yellow-500" };
if (value <= 2.0) return { text: "很爽", color: "text-green-500" };
return { text: "爽到爆炸", color: "text-purple-500" };
};
const RadioGroup = ({ label, name, value, onChange, options }: {
label: string;
name: string;
value: string;
onChange: (name: string, value: string) => void;
options: Array<{ label: string; value: string; }>;
}) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">{label}</label>
<div className="grid 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 font-medium'
: 'bg-gray-50 hover:bg-gray-100'}`}
onClick={() => onChange(name, option.value)}
type="button"
>
{option.label}
</button>
))}
</div>
</div>
);
return (
<div className="max-w-4xl mx-auto p-4 space-y-8">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold text-gray-800">
b班上得值不值·
</h1>
<div className="text-sm text-gray-500">
</div>
</div>
<div className="bg-white rounded-lg shadow-lg">
<div className="p-6 space-y-6">
{/* 薪资与工作时间 */}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<div className="flex items-center gap-2 mt-1">
<Wallet className="w-4 h-4 text-gray-500" />
<input
type="number"
value={formData.annualSalary}
onChange={(e) => handleInputChange('annualSalary', e.target.value)}
placeholder="税前年薪"
className="block w-full rounded-md border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<input
type="number"
min="1"
max="7"
value={formData.workDaysPerWeek}
onChange={(e) => handleInputChange('workDaysPerWeek', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></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 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></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 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></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 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700"></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 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<input
type="number"
value={formData.breakHours}
onChange={(e) => handleInputChange('breakHours', e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
</div>
<div className="border-t border-gray-200 my-6"></div>
{/* 环境系数 */}
<div className="space-y-4">
<RadioGroup
label="工作环境"
name="workEnvironment"
value={formData.workEnvironment}
onChange={handleInputChange}
options={[
{ label: '普通环境', value: '1.0' },
{ label: '偏僻地区', value: '0.8' },
{ label: '工厂工地', value: '0.9' },
{ label: 'CBD', value: '1.1' },
]}
/>
<RadioGroup
label="异性环境"
name="heterogeneity"
value={formData.heterogeneity}
onChange={handleInputChange}
options={[
{ label: '一般', value: '1.0' },
{ label: '没好看的', value: '0.9' },
{ label: '很多好看的', value: '1.1' },
]}
/>
<RadioGroup
label="同事环境"
name="teamwork"
value={formData.teamwork}
onChange={handleInputChange}
options={[
{ label: '普通同事', value: '1.0' },
{ label: '烦人同事多', value: '0.95' },
{ label: '优秀同事多', value: '1.05' },
]}
/>
<RadioGroup
label="学历要求"
name="education"
value={formData.education}
onChange={handleInputChange}
options={[
{ label: '专科及以下', value: '0.8' },
{ label: '普通本科', value: '1.0' },
{ label: '211本科', value: '1.2' },
{ label: '普通硕士', value: '1.4' },
{ label: '985硕士', value: '1.6' },
{ label: '普通博士', value: '1.8' },
{ label: '211博士', value: '2.0' },
]}
/>
</div>
</div>
</div>
<div className="bg-gray-50 border rounded-lg p-4">
<div className="grid grid-cols-3 gap-4">
<div>
<div className="text-sm text-gray-500"></div>
<div className="text-lg font-medium">{calculateWorkingDays()}</div>
</div>
<div>
<div className="text-sm text-gray-500"></div>
<div className="text-lg font-medium">¥{calculateDailySalary().toFixed(2)}</div>
</div>
<div>
<div className="text-sm text-gray-500"></div>
<div className={`text-lg font-medium ${getValueAssessment().color}`}>
{value.toFixed(2)} ({getValueAssessment().text})
</div>
</div>
</div>
</div>
</div>
);
};
export default SalaryCalculator;

10
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "worth-calculator",
"version": "0.1.0",
"dependencies": {
"lucide-react": "^0.454.0",
"next": "15.0.2",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028"
@@ -3733,6 +3734,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/lucide-react": {
"version": "0.454.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz",
"integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",

View File

@@ -9,18 +9,19 @@
"lint": "next lint"
},
"dependencies": {
"lucide-react": "^0.454.0",
"next": "15.0.2",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028",
"next": "15.0.2"
"react-dom": "19.0.0-rc-02c0e824-20241028"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.2",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "15.0.2"
"typescript": "^5"
}
}