├── .nvmrc ├── test.txt ├── title.png ├── images ├── en.png ├── ja.png └── zh.png ├── test └── test.txt ├── app ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── zh │ ├── page.tsx │ └── [country] │ │ └── page.tsx ├── page.tsx ├── robots.ts ├── head.tsx ├── manifest.ts ├── globals.css ├── sitemap.ts ├── layout.tsx ├── [country] │ └── page.tsx └── share │ └── page.tsx ├── public ├── logo.png ├── title.png ├── banner.png ├── favicon.ico ├── formula.png ├── mainpage.png ├── website.png ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg ├── next.svg └── logo.svg ├── .eslintrc.json ├── postcss.config.mjs ├── next.config.ts ├── components ├── countryToEmoji.ts ├── HtmlLangProvider.tsx ├── LanguageRedirect.tsx ├── GoogleBannerAd.tsx ├── HorizontalBanner.tsx ├── LanguageSwitcher.tsx ├── CountryFooter.tsx ├── LanguageVersionSwitcher.tsx ├── VerticalAd.tsx ├── pppFactors.ts ├── countryNames.ts ├── ShareCard.tsx └── LanguageContext.tsx ├── tailwind.config.ts ├── .gitignore ├── tsconfig.json ├── utils └── adConfig.ts ├── package.json ├── LICENSE ├── worthjob.pdftool.cc.conf └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/test.txt -------------------------------------------------------------------------------- /title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/title.png -------------------------------------------------------------------------------- /images/en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/images/en.png -------------------------------------------------------------------------------- /images/ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/images/ja.png -------------------------------------------------------------------------------- /images/zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/images/zh.png -------------------------------------------------------------------------------- /test/test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/test/test.txt -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/app/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/public/logo.png -------------------------------------------------------------------------------- /public/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/public/title.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/public/banner.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/public/favicon.ico -------------------------------------------------------------------------------- /public/formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/public/formula.png -------------------------------------------------------------------------------- /public/mainpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/public/mainpage.png -------------------------------------------------------------------------------- /public/website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/public/website.png -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phalapi/worth-calculator/main/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-unused-vars": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/zh/page.tsx: -------------------------------------------------------------------------------- 1 | import Calculator from '@/components/calculator'; 2 | 3 | export default function ChineseHome() { 4 | return ( 5 | <> 6 |
7 | 8 |
9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | output: 'export', 6 | trailingSlash: true, 7 | images: { 8 | unoptimized: true 9 | } 10 | }; 11 | 12 | export default nextConfig; 13 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Calculator from '@/components/calculator'; 2 | 3 | export default function Home() { 4 | return ( 5 | <> 6 | 7 |
8 | 9 |
10 | 11 | ); 12 | } -------------------------------------------------------------------------------- /components/countryToEmoji.ts: -------------------------------------------------------------------------------- 1 | // ISO 3166-1 alpha-2 转 emoji 国旗 2 | export default function countryToEmoji(code: string): string { 3 | if (!code || code.length !== 2) return "🏳️"; 4 | return String.fromCodePoint(...code.toUpperCase().split('').map(c => 0x1F1E6 + c.charCodeAt(0) - 65)); 5 | } 6 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next' 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: '*', 7 | allow: '/', 8 | disallow: ['/api/', '/_next/', '/admin/'], 9 | }, 10 | sitemap: 'https://worthjob.pdftool.cc/sitemap.xml', 11 | } 12 | } -------------------------------------------------------------------------------- /app/head.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Job Worth Calculator | 这班上得值不值', 5 | description: '计算你的工作价值,了解薪资性价比。支持全球180+国家的购买力平价(PPP)计算,输入年薪即可获得详细的工作价值分析报告。Job Worth Calculator', 6 | keywords: '工作价值, 薪资计算, 购买力平价, PPP, 工作性价比, 年薪计算, 全球薪资对比, jobworth, 这班上得值不值, salary worth, salary worth calculator, salary worth calculator', 7 | }; -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /components/HtmlLangProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from 'react'; 4 | import { useLanguage } from './LanguageContext'; 5 | 6 | export const HtmlLangProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 7 | const { language } = useLanguage(); 8 | 9 | useEffect(() => { 10 | // 根据当前语言设置html的lang属性 11 | const htmlElement = document.documentElement; 12 | const langMap = { 13 | 'en': 'en', 14 | 'zh': 'zh-CN', 15 | 'ja': 'ja' 16 | }; 17 | 18 | htmlElement.lang = langMap[language] || 'en'; 19 | }, [language]); 20 | 21 | return <>{children}; 22 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | deploy.sh 43 | 44 | -------------------------------------------------------------------------------- /components/LanguageRedirect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from 'react'; 4 | import { useLanguage } from './LanguageContext'; 5 | import { usePathname } from 'next/navigation'; 6 | 7 | export const LanguageRedirect: React.FC = () => { 8 | const { setLanguage } = useLanguage(); 9 | const pathname = usePathname(); 10 | 11 | useEffect(() => { 12 | // 根据URL路径自动设置语言 13 | if (pathname.startsWith('/zh/')) { 14 | setLanguage('zh'); 15 | } else if (pathname.startsWith('/ja/')) { 16 | setLanguage('ja'); 17 | } else { 18 | // 默认英文 19 | setLanguage('en'); 20 | } 21 | }, [pathname, setLanguage]); 22 | 23 | return null; 24 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/manifest.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next' 2 | 3 | export default function manifest(): MetadataRoute.Manifest { 4 | return { 5 | name: 'Job Worth Calculator | Is My Job Worth the Grind?', 6 | short_name: 'Job Worth', 7 | description: 'Calculate your job worth and understand salary value with PPP support for 180+ countries', 8 | start_url: '/', 9 | display: 'standalone', 10 | background_color: '#ffffff', 11 | theme_color: '#3b82f6', 12 | icons: [ 13 | { 14 | src: '/favicon.ico', 15 | sizes: 'any', 16 | type: 'image/x-icon', 17 | }, 18 | ], 19 | categories: ['productivity', 'business', 'finance'], 20 | lang: 'en', 21 | dir: 'ltr', 22 | } 23 | } -------------------------------------------------------------------------------- /utils/adConfig.ts: -------------------------------------------------------------------------------- 1 | // 广告配置工具函数 2 | 3 | export function isAdEnabled(): boolean { 4 | // 检查是否启用广告 5 | const enabled = process.env.NEXT_PUBLIC_AD_ENABLED; 6 | if (enabled === 'false') { 7 | return false; 8 | } 9 | 10 | // 检查广告结束时间 11 | const endTimeStr = process.env.NEXT_PUBLIC_AD_END_TIME; 12 | if (!endTimeStr) { 13 | // 如果没有设置结束时间,默认显示广告 14 | return true; 15 | } 16 | 17 | try { 18 | const endTime = new Date(endTimeStr); 19 | const now = new Date(); 20 | 21 | // 如果当前时间超过了结束时间,则不显示广告 22 | return now <= endTime; 23 | } catch (error) { 24 | console.error('Invalid ad end time format:', endTimeStr); 25 | return true; // 时间格式错误时默认显示广告 26 | } 27 | } 28 | 29 | export function getAdLink(): string { 30 | return process.env.NEXT_PUBLIC_AD_LINK || 'https://www.google.com'; 31 | } -------------------------------------------------------------------------------- /components/GoogleBannerAd.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | export default function GoogleBannerAd() { 5 | const adRef = useRef(null); 6 | 7 | useEffect(() => { 8 | if (typeof window !== "undefined") { 9 | try { 10 | // @ts-ignore 11 | (window.adsbygoogle = window.adsbygoogle || []).push({}); 12 | } catch (e) { 13 | // 忽略广告加载错误 14 | } 15 | } 16 | }, []); 17 | 18 | return ( 19 |
20 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worth-calculator", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build --no-lint", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@notionhq/client": "^2.3.0", 13 | "@vercel/analytics": "^1.5.0", 14 | "html-to-image": "^1.11.13", 15 | "html2canvas": "^1.4.1", 16 | "lucide-react": "^0.454.0", 17 | "next": "15.0.2", 18 | "qrcode": "^1.5.4", 19 | "react": "19.0.0-rc-02c0e824-20241028", 20 | "react-dom": "19.0.0-rc-02c0e824-20241028" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20", 24 | "@types/qrcode": "^1.5.5", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | 28 | "eslint": "^8", 29 | "eslint-config-next": "15.0.2", 30 | "postcss": "^8", 31 | "tailwindcss": "^3.4.1", 32 | "typescript": "^5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zylan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /worthjob.pdftool.cc.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name worthjob.pdftool.cc; 4 | # 强制跳转到 https 5 | return 301 https://$host$request_uri; 6 | } 7 | 8 | server { 9 | listen 443 ssl http2; 10 | server_name worthjob.pdftool.cc; 11 | 12 | # SSL 证书配置(请替换为你自己的证书路径) 13 | ssl_certificate /etc/nginx/cert/pdftool.cc.pem; 14 | ssl_certificate_key /etc/nginx/cert/pdftool.cc.key; 15 | 16 | # 静态文件根目录(根据实际部署目录调整) 17 | root /home/dogstaradmin/projects/worthjob.pdftool.cc/out; 18 | 19 | # 启用 gzip 压缩 20 | gzip on; 21 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; 22 | gzip_min_length 1024; 23 | 24 | # 默认首页 25 | index index.html; 26 | 27 | # 支持 Nuxt3 静态多语言/history路由 28 | location / { 29 | try_files $uri $uri/ /index.html; 30 | } 31 | 32 | # 支持多语言路径(如 /zh/merge、/es/tools 等) 33 | location ~ ^/(en|zh|es|fr|hi|ar|ru)(/.*)?$ { 34 | try_files $uri $uri/ /index.html; 35 | } 36 | 37 | # 静态资源缓存 38 | location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|woff|woff2|ttf|eot)$ { 39 | expires 30d; 40 | access_log off; 41 | add_header Cache-Control "public"; 42 | } 43 | 44 | # 禁止访问隐藏文件 45 | location ~ /\. { 46 | deny all; 47 | } 48 | 49 | # 错误页 50 | error_page 404 /index.html; 51 | } -------------------------------------------------------------------------------- /components/HorizontalBanner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import Image from 'next/image'; 5 | import { isAdEnabled, getAdLink } from '@/utils/adConfig'; 6 | 7 | export default function HorizontalBanner() { 8 | const [imageExists, setImageExists] = useState(false); 9 | 10 | useEffect(() => { 11 | // 检查图片是否存在 12 | const img = new window.Image(); 13 | img.onload = () => setImageExists(true); 14 | img.onerror = () => setImageExists(false); 15 | img.src = '/banner.png'; 16 | }, []); 17 | 18 | // 如果广告未启用或已过期,不显示 19 | if (!isAdEnabled()) { 20 | return null; 21 | } 22 | 23 | // 如果图片不存在,不显示 24 | if (!imageExists) { 25 | return null; 26 | } 27 | 28 | return ( 29 |
30 |
31 | {/* 横向 Banner - 7:1 比例 */} 32 | 41 | {/* 实际广告图片 */} 42 | 横幅广告 49 | 50 |
51 |
52 | ); 53 | } -------------------------------------------------------------------------------- /components/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { useLanguage } from './LanguageContext'; 5 | 6 | export const LanguageSwitcher: React.FC = () => { 7 | const { language, setLanguage } = useLanguage(); 8 | 9 | return ( 10 |
11 | 21 | 31 | 41 |
42 | ); 43 | }; -------------------------------------------------------------------------------- /components/CountryFooter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { useLanguage } from "./LanguageContext"; 5 | import { countryNames } from "./LanguageContext"; 6 | import countryToEmoji from "@/components/countryToEmoji"; 7 | import pppFactors from "@/components/pppFactors"; 8 | 9 | // 获取国家英文名(用于SEO链接) 10 | function getCountrySlug(code: string, lang: string): string { 11 | // 优先用英文名,转小写、替换空格为-,去除特殊字符 12 | let name = countryNames["en"][code] || code; 13 | return name.replace(/[^a-zA-Z0-9 ]/g, "").replace(/\s+/g, "-").toLowerCase(); 14 | } 15 | 16 | const CountryFooter: React.FC = () => { 17 | const { language } = useLanguage(); 18 | console.log('CountryFooter language', language); 19 | // 只展示有PPP数据的国家 20 | const countries = Object.keys(pppFactors) 21 | .map((code) => ({ 22 | code, 23 | name: countryNames[language][code] || countryNames["en"][code] || code, 24 | ppp: pppFactors[code], 25 | emoji: countryToEmoji(code), 26 | slug: getCountrySlug(code, language), 27 | })) 28 | .filter((c) => c.ppp) 29 | .sort((a, b) => b.ppp - a.ppp); 30 | 31 | return ( 32 |
33 | {countries.map((c) => ( 34 | 39 | {c.emoji} 40 | {c.name} 41 | PPP: {c.ppp} 42 | 43 | ))} 44 |
45 | ); 46 | }; 47 | 48 | export default CountryFooter; 49 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | /* 页面和卡片的过渡动画 */ 24 | @keyframes fadeIn { 25 | from { 26 | opacity: 0; 27 | transform: translateY(20px); 28 | } 29 | to { 30 | opacity: 1; 31 | transform: translateY(0); 32 | } 33 | } 34 | 35 | .animate-fadeIn { 36 | animation: fadeIn 0.5s ease-out forwards; 37 | } 38 | 39 | /* 卡片翻页效果 */ 40 | @keyframes flipIn { 41 | from { 42 | opacity: 0; 43 | transform: rotateY(-10deg) translateZ(-100px); 44 | } 45 | to { 46 | opacity: 1; 47 | transform: rotateY(0) translateZ(0); 48 | } 49 | } 50 | 51 | .animate-flipIn { 52 | animation: flipIn 0.6s ease-out forwards; 53 | } 54 | 55 | /* 按钮点击效果 */ 56 | .btn-pulse { 57 | position: relative; 58 | overflow: hidden; 59 | } 60 | 61 | .btn-pulse::after { 62 | content: ''; 63 | position: absolute; 64 | top: 50%; 65 | left: 50%; 66 | width: 5px; 67 | height: 5px; 68 | background: rgba(255, 255, 255, 0.5); 69 | opacity: 0; 70 | border-radius: 100%; 71 | transform: scale(1, 1) translate(-50%, -50%); 72 | transform-origin: 50% 50%; 73 | } 74 | 75 | .btn-pulse:focus::after { 76 | animation: ripple 1s ease-out; 77 | } 78 | 79 | @keyframes ripple { 80 | 0% { 81 | transform: scale(0, 0); 82 | opacity: 0.5; 83 | } 84 | 100% { 85 | transform: scale(50, 50); 86 | opacity: 0; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next' 2 | import pppFactors from '@/components/pppFactors' 3 | import { countryNames } from '@/components/countryNames' 4 | 5 | export default function sitemap(): MetadataRoute.Sitemap { 6 | const baseUrl = 'https://worthjob.pdftool.cc' 7 | 8 | // 基础页面 9 | const basePages = [ 10 | { 11 | url: baseUrl, 12 | lastModified: new Date(), 13 | changeFrequency: 'weekly' as const, 14 | priority: 1, 15 | }, 16 | { 17 | url: `${baseUrl}/zh`, 18 | lastModified: new Date(), 19 | changeFrequency: 'weekly' as const, 20 | priority: 0.9, 21 | }, 22 | { 23 | url: `${baseUrl}/share`, 24 | lastModified: new Date(), 25 | changeFrequency: 'monthly' as const, 26 | priority: 0.7, 27 | }, 28 | ] 29 | 30 | // 生成国家页面 31 | const countryPages = Object.keys(pppFactors) 32 | .filter(code => pppFactors[code]) 33 | .flatMap(code => { 34 | const englishName = countryNames.en[code] 35 | const chineseName = countryNames.zh[code] 36 | 37 | const pages = [] 38 | 39 | // 英文版本(默认) 40 | if (englishName) { 41 | const englishSlug = englishName.replace(/[^a-zA-Z0-9 ]/g, "").replace(/\s+/g, "-").toLowerCase() 42 | pages.push({ 43 | url: `${baseUrl}/${englishSlug}`, 44 | lastModified: new Date(), 45 | changeFrequency: 'monthly' as const, 46 | priority: 0.8, 47 | }) 48 | } 49 | 50 | // 中文版本 51 | if (chineseName) { 52 | const chineseSlug = chineseName.replace(/[^a-zA-Z0-9 ]/g, "").replace(/\s+/g, "-").toLowerCase() 53 | pages.push({ 54 | url: `${baseUrl}/zh/${chineseSlug}`, 55 | lastModified: new Date(), 56 | changeFrequency: 'monthly' as const, 57 | priority: 0.7, 58 | }) 59 | } 60 | 61 | return pages 62 | }) 63 | 64 | return [...basePages, ...countryPages] 65 | } -------------------------------------------------------------------------------- /components/LanguageVersionSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { usePathname } from 'next/navigation'; 5 | import Link from 'next/link'; 6 | 7 | export const LanguageVersionSwitcher: React.FC = () => { 8 | const pathname = usePathname(); 9 | 10 | // 获取当前路径对应的其他语言版本 11 | const getLanguageVersions = () => { 12 | const isChineseVersion = pathname.startsWith('/zh/'); 13 | const isJapaneseVersion = pathname.startsWith('/ja/'); 14 | 15 | if (isChineseVersion) { 16 | // 中文版本 -> 英文版本 17 | const englishPath = pathname.replace('/zh/', '/'); 18 | return { 19 | current: '中文', 20 | other: 'English', 21 | otherPath: englishPath, 22 | currentPath: pathname 23 | }; 24 | } else if (isJapaneseVersion) { 25 | // 日文版本 -> 英文版本 26 | const englishPath = pathname.replace('/ja/', '/'); 27 | return { 28 | current: '日本語', 29 | other: 'English', 30 | otherPath: englishPath, 31 | currentPath: pathname 32 | }; 33 | } else { 34 | // 英文版本 -> 中文版本 35 | const chinesePath = pathname.startsWith('/') ? `/zh${pathname}` : `/zh/${pathname}`; 36 | return { 37 | current: 'English', 38 | other: '中文', 39 | otherPath: chinesePath, 40 | currentPath: pathname 41 | }; 42 | } 43 | }; 44 | 45 | const versions = getLanguageVersions(); 46 | 47 | return ( 48 |
49 | Language: 50 |
51 | 52 | {versions.current} 53 | 54 | 58 | {versions.other} 59 | 60 |
61 |
62 | ); 63 | }; -------------------------------------------------------------------------------- /components/VerticalAd.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import Image from 'next/image'; 5 | import { X } from 'lucide-react'; 6 | import { isAdEnabled, getAdLink } from '@/utils/adConfig'; 7 | 8 | export default function VerticalAd() { 9 | const [isVisible, setIsVisible] = useState(false); 10 | const [imageExists, setImageExists] = useState(false); 11 | 12 | useEffect(() => { 13 | // 检查图片是否存在 14 | const img = new window.Image(); 15 | img.onload = () => setImageExists(true); 16 | img.onerror = () => setImageExists(false); 17 | img.src = '/mainpage.png'; 18 | }, []); 19 | 20 | useEffect(() => { 21 | // 检查广告是否应该显示 22 | if (!isAdEnabled() || !imageExists) { 23 | return; 24 | } 25 | 26 | // 延迟显示广告,避免影响首屏加载 27 | const timer = setTimeout(() => { 28 | setIsVisible(true); 29 | }, 1000); 30 | 31 | return () => clearTimeout(timer); 32 | }, [imageExists]); 33 | 34 | const handleClose = () => { 35 | setIsVisible(false); 36 | }; 37 | 38 | if (!isVisible) return null; 39 | 40 | return ( 41 |
42 |
43 | {/* 关闭按钮 */} 44 | 51 | 52 | {/* 广告内容 - 3:5 比例(宽:高) */} 53 | 63 | {/* 实际广告图片 */} 64 | 广告 71 | 72 |
73 |
74 | ); 75 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { LanguageProvider } from "@/components/LanguageContext"; 5 | import CountryFooter from "@/components/CountryFooter"; 6 | import { Analytics } from "@vercel/analytics/next"; 7 | 8 | const geistSans = localFont({ 9 | src: "./fonts/GeistVF.woff", 10 | variable: "--font-geist-sans", 11 | weight: "100 900", 12 | }); 13 | const geistMono = localFont({ 14 | src: "./fonts/GeistMonoVF.woff", 15 | variable: "--font-geist-mono", 16 | weight: "100 900", 17 | }); 18 | 19 | export const metadata: Metadata = { 20 | title: { 21 | default: "Job Worth Calculator | 这班上得值不值", 22 | template: "%s" 23 | }, 24 | alternates: { 25 | languages: { 26 | "en-US": "/en", 27 | "zh-CN": "/", 28 | }, 29 | }, 30 | description: "计算你的工作价值,了解薪资性价比。支持全球180+国家的购买力平价(PPP)计算,输入年薪即可获得详细的工作价值分析报告。Job Worth Calculator", 31 | }; 32 | 33 | export default function RootLayout({ 34 | children, 35 | }: Readonly<{ 36 | children: React.ReactNode; 37 | }>) { 38 | return ( 39 | 40 | 41 | 42 | 43 | {/* Google Analytics */} 44 | 45 |