Repository: daijinhai/StayFocused Branch: main Commit: f4bc73ee780c Files: 63 Total size: 154.7 KB Directory structure: gitextract_flgb_r6k/ ├── .gitignore ├── README.md ├── electron/ │ ├── main.cjs │ └── preload.cjs ├── eslint.config.js ├── index.html ├── package.json ├── postcss.config.js ├── public/ │ ├── icon.icns │ ├── robots.txt │ └── sitemap.xml ├── scripts/ │ ├── generate-icons.cjs │ ├── generate-icons.js │ ├── generate-win-icon.cjs │ ├── seo-check.cjs │ └── seo-check.js ├── src/ │ ├── App.tsx │ ├── components/ │ │ ├── AlertDialog.tsx │ │ ├── LanguageSwitcher.tsx │ │ ├── Layout.tsx │ │ ├── SEOHead.tsx │ │ ├── SavedMixes.tsx │ │ ├── SoundButton.tsx │ │ ├── SoundIcon.tsx │ │ ├── ThemeSelector.tsx │ │ ├── ThemeToggle.tsx │ │ └── Timer.tsx │ ├── data/ │ │ ├── categories.ts │ │ ├── categoryMap.ts │ │ ├── seoConfig.ts │ │ ├── sounds/ │ │ │ ├── animals.ts │ │ │ ├── city.ts │ │ │ ├── index.ts │ │ │ ├── nature.ts │ │ │ └── rain.ts │ │ ├── sounds.ts │ │ └── themes.ts │ ├── hooks/ │ │ ├── useOnClickOutside.ts │ │ ├── useSEO.ts │ │ └── useWebVitals.ts │ ├── i18n.ts │ ├── index.css │ ├── locales/ │ │ ├── en/ │ │ │ └── translation.json │ │ ├── en.json │ │ ├── zh/ │ │ │ └── translation.json │ │ └── zh.json │ ├── main.tsx │ ├── store/ │ │ ├── timerStore.ts │ │ ├── useStore.ts │ │ └── useThemeStore.ts │ ├── themes/ │ │ └── index.ts │ ├── types/ │ │ ├── index.ts │ │ └── theme.ts │ ├── types.ts │ ├── utils/ │ │ ├── analytics.ts │ │ ├── audio.ts │ │ └── seoAudit.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # 构建输出 release release/ # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? # Electron 构建相关 public/icons-win .npmrc ================================================ FILE: README.md ================================================ # StayFocused - 专注时钟 & 白噪音应用
StayFocused Logo

打造专属你的专注与放松环境

StayFocused 是一个帮助你保持专注的应用,通过自然声音和白噪音创造理想的工作与放松环境。支持Web网页版和桌面应用程序,随时随地提升你的专注力。 *Web体验地址:* https://shutong.work/ ## 📖 目录 - [主要功能](#-主要功能) - [下载安装](#-下载安装) - [使用指南](#-使用指南) - [桌面应用特性](#-桌面应用特性) - [开发指南](#-开发指南) - [技术栈](#-技术栈) - [构建发布](#-构建发布) - [声音资源](#-声音资源) - [常见问题](#-常见问题) - [贡献指南](#-贡献指南) - [许可协议](#-许可协议) *体验网址*:https://shutong.work/ ## ✨ 主要功能 - 🎵 **丰富的白噪音库**:包含20+种高质量环境音效,分为自然、雨声、城市三大类 - 🎚️ **自定义声音混音**:可以同时播放多种声音,并调整各声音的音量 - ⏲️ **专注计时器**:内置25/45/60分钟的专注时钟,支持自定义时长 - 💾 **混音保存与加载**:可保存常用的声音组合,随时加载使用 - 🎨 **多主题切换**:提供6种主题颜色,包括温暖橙、极简白、自然绿等 - 🌙 **暗色/亮色模式**:适应不同光线环境,保护视力 - 🖥️ **跨平台支持**:支持Web网页版和桌面应用(macOS/Windows/Linux) - 📱 **响应式设计**:完美适配各种屏幕尺寸 ## 📥 下载安装 ### Web版本 直接访问网站体验:[https://shutong.work/](https://shutong.work/) ### 桌面应用版本 #### macOS 1. 下载适合您Mac芯片的安装包: - Apple Silicon (M1/M2/M3): [StayFocused-1.0.0-arm64.dmg](https://github.com/daijinhai/StayFocused/releases/download/V1.0.0/StayFocused-1.0.0-arm64.dmg) 2. 打开下载的DMG文件 3. 将StayFocused应用拖到Applications文件夹 4. 首次启动时,如果遇到"无法验证开发者"的提示,请前往系统偏好设置→安全性与隐私,点击"仍要打开" #### 其他系统版本(请自行源码打包) ## 🎮 使用指南 ### 播放白噪音 1. 选择喜欢的声音类别(自然、雨声、城市) 2. 点击声音图标开始播放 3. 使用滑块调整音量 4. 可以同时播放多种声音,创建个性化混音效果 ### 使用专注计时器 1. 在右侧面板选择预设时长(25/45/60分钟)或自定义时间 2. 点击"开始"按钮启动计时器 3. 计时结束后会收到通知提醒 4. 可以选择是否开启结束提示音 ### 保存和加载混音 1. 创建喜欢的声音组合后,在右侧"我的混音"区域点击"保存当前混音" 2. 输入混音名称保存 3. 随时点击已保存的混音名称加载使用 4. 通过删除按钮移除不需要的混音 ### 切换主题 1. 点击右上角的主题设置按钮 2. 选择喜欢的颜色主题 3. 主题设置会自动保存 ## 🖥️ 桌面应用特性 桌面版StayFocused相比Web版本,提供以下额外功能: 1. **离线使用**:无需网络连接,随时随地使用 2. **系统通知**:专注时间结束时发送系统级通知 3. **后台播放**:即使最小化窗口,声音也会继续播放 4. **自启动选项**:可设置为开机自动启动(参见设置) 5. **更流畅的体验**:本地运行,响应更快,无加载延迟 ## 🛠️ 开发指南 ### 环境要求 - Node.js 16.0.0+(推荐使用Node.js 18 LTS) - npm 7.0.0+ ### 开发环境搭建 ```bash # 克隆仓库 git clone https://github.com/daijinhai/StayFocused.git cd StayFocused # 安装依赖 npm install # 启动Web开发服务器 npm run dev # 启动Electron开发模式 npm run electron:dev ``` ### 项目结构 ``` StayFocused/ ├── electron/ # Electron相关代码 │ ├── main.cjs # 主进程 │ └── preload.cjs # 预加载脚本 ├── public/ # 静态资源 │ ├── icons/ # 应用图标 │ └── sounds/ # 声音文件 ├── scripts/ # 构建脚本 ├── src/ # 源代码 │ ├── components/ # React组件 │ ├── data/ # 数据定义 │ ├── store/ # 状态管理 │ ├── utils/ # 工具函数 │ └── App.tsx # 应用入口 ├── package.json # 项目配置 └── vite.config.ts # Vite配置 ``` ## 🚀 技术栈 - **前端框架**: React 18 + TypeScript - **状态管理**: Zustand - **样式方案**: Tailwind CSS - **构建工具**: Vite - **桌面应用**: Electron - **国际化**: i18next - **图标**: Lucide React ## 📦 构建发布 ### Web版本构建 ```bash # 构建Web版本 npm run build # 预览构建结果 npm run preview ``` 构建输出位于 `dist/` 目录,可部署到任何静态网站托管服务。 ### 桌面应用构建 #### macOS应用构建 ```bash # 构建macOS ARM版本(Apple Silicon) npm run dist:mac # 构建macOS Intel版本 npm run dist:mac-intel # 构建所有平台版本(需要在对应平台上执行) npm run dist ``` 构建输出位于 `release/` 目录: - **macOS ARM**: `release/StayFocused-1.0.0-arm64.dmg` - **macOS Intel**: `release/StayFocused-1.0.0-x64.dmg` #### Windows应用构建 Windows应用构建需要在Windows系统上执行,或使用虚拟机/容器。以下是构建步骤: 1. **环境准备** ```bash # 在Windows系统上,打开PowerShell或CMD # 安装Node.js 18 LTS和npm # 克隆仓库 git clone https://github.com/daijinhai/StayFocused.git cd StayFocused # 安装依赖 npm install ``` 2. **Windows图标准备** Windows应用需要.ico格式的图标文件。在Windows上,需要执行以下步骤: ```bash # 安装图标转换工具 npm install -g png-to-ico # 在项目根目录创建Windows图标 mkdir -p public/icons-win png-to-ico public/icons/icon-256.png > public/icons-win/icon.ico ``` 或者在scripts目录下创建一个Windows图标生成脚本`generate-win-icon.js`: ```javascript const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // 确保目录存在 const winIconDir = path.join(__dirname, '../public/icons-win'); if (!fs.existsSync(winIconDir)) { fs.mkdirSync(winIconDir, { recursive: true }); } // 使用ImageMagick转换图标 try { execSync(`magick convert "${path.join(__dirname, '../public/icons/icon-256.png')}" "${path.join(winIconDir, 'icon.ico')}"`); console.log('Windows图标生成成功!'); } catch (error) { console.error('生成Windows图标失败:', error.message); console.log('请确保已安装ImageMagick: https://imagemagick.org/script/download.php'); } ``` 3. **修改package.json** 在package.json中添加Windows构建脚本和配置: ```json "scripts": { "dist:win": "npm run build && electron-builder --win --x64", "dist:win-ia32": "npm run build && electron-builder --win --ia32" }, "build": { "win": { "icon": "public/icons-win/icon.ico", "target": ["nsis"], "artifactName": "${productName}-${version}-${arch}.${ext}", "publisherName": "YourName" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true } } ``` 4. **Windows特殊调整** 修改`electron/main.cjs`文件,添加Windows平台特定代码: ```javascript // 在适当位置添加以下代码 if (process.platform === 'win32') { // Windows平台的一些特殊处理 app.setAppUserModelId('com.stayfocused.app'); // 为Windows通知设置应用ID } ``` 5. **执行Windows构建** ```bash # 构建64位Windows应用 npm run dist:win # 构建32位Windows应用(如需支持较旧系统) npm run dist:win-ia32 ``` 构建输出位于`release/`目录: - **Windows 64位**: `release/StayFocused-1.0.0-x64.exe` - **Windows 32位**: `release/StayFocused-1.0.0-ia32.exe` 6. **Windows构建可能遇到的问题及解决方案** - **权限问题**:确保以管理员身份运行命令提示符或PowerShell - **杀毒软件干扰**:构建过程可能被杀毒软件阻止,可以临时禁用或添加例外 - **依赖库缺失**:某些Windows特定的库可能需要手动安装: ```bash npm install --save-dev windows-build-tools ``` - **路径过长错误**:Windows有路径长度限制,确保项目路径不要太深 ### 自定义图标 项目包含图标生成脚本,可生成各种尺寸的应用图标: ```bash # 安装依赖 npm install -g sharp # 生成图标 node scripts/generate-icons.cjs ``` ## 🎵 声音资源 所有声音均来自 [Freesound](https://freesound.org/),遵循 Creative Commons 许可协议。 ### 自然 | 中文名 | 文件名 | 下载地址 | |-------|--------|----------| | 森林鸟鸣 | forest-birds.mp3 | https://freesound.org/people/sonidosreales245/sounds/518494/ | | 海浪 | waves.mp3 | https://freesound.org/people/Pfannkuchn/sounds/360631/ | | 溪流 | creek.mp3 | https://freesound.org/people/juskiddink/sounds/78955/ | | 微风 | wind.mp3 | https://freesound.org/people/kangaroovindaloo/sounds/205966/ | | 树叶沙沙 | leaves-rustling.mp3 | https://freesound.org/people/InspectorJ/sounds/365915/ | | 瀑布 | waterfall.mp3 | https://freesound.org/people/InspectorJ/sounds/364309/ | | 篝火 | bonfire.mp3 | https://freesound.org/people/aerror/sounds/350757/ | | 海滩 | beach.mp3 | https://freesound.org/people/Noted451/sounds/531015/ | | 夜晚森林 | forest-night.mp3 | https://freesound.org/people/sagetyrtle/sounds/124911/ | ### 雨声 | 中文名 | 文件名 | 下载地址 | |-------|--------|----------| | 小雨 | rain-light.mp3 | https://freesound.org/people/tim.kahn/sounds/174031/ | | 大雨 | rain-heavy.mp3 | https://freesound.org/people/D%20W/sounds/136971/ | | 屋檐雨声 | rain-roof.mp3 | https://freesound.org/people/Kinoton/sounds/351897/ | | 窗外雨声 | rain-window.mp3 | https://freesound.org/people/InspectorJ/sounds/346642/ | | 雷雨 | rain-thunder.mp3 | https://freesound.org/people/lonemonk/sounds/62015/ | | 雨打树叶 | rain-leaves.mp3 | https://freesound.org/people/reinsamba/sounds/58256/ | | 雨水潭 | rain-puddle.mp3 | https://freesound.org/people/InspectorJ/sounds/401273/ | ### 城市 | 中文名 | 文件名 | 下载地址 | |-------|--------|----------| | 城市交通 | city-traffic.mp3 | https://freesound.org/people/InspectorJ/sounds/475339/ | | 咖啡馆 | cafe.mp3 | https://freesound.org/people/FoolBoyMedia/sounds/264282/ | | 键盘声 | keyboard.mp3 | https://freesound.org/people/Dizzon/sounds/324540/ | | 地铁 | subway.mp3 | https://freesound.org/people/psubhashish/sounds/172496/ | | 公园 | park.mp3 | https://freesound.org/people/InspectorJ/sounds/421791/ | | 火车 | train.mp3 | https://freesound.org/people/danielnieto7/sounds/135873/ | 所有声音均经过精心挑选,确保: - 录音质量高(至少 44.1kHz,16bit) - 环境音真实自然 - 适合循环播放 - 背景噪音小 - 时长适中(通常 1-3 分钟) ## ❓ 常见问题 ### Q: 桌面应用无法播放声音怎么办? A: 请确保您的系统音量已开启,并且没有其他应用程序独占音频设备。如果问题仍然存在,尝试重启应用程序。 ### Q: 如何在系统启动时自动启动应用? A: 目前需要通过系统设置添加启动项: - **macOS**: 系统设置 → 用户与群组 → 登录项 → 添加StayFocused - **Windows**: 右键应用快捷方式 → 属性 → 快捷方式 → 勾选"开机启动" - **Linux**: 根据不同发行版有不同方法,一般在系统设置中的"启动应用程序"中添加 ### Q: 计时器结束没有收到通知? A: 请确保已授予应用发送通知的权限: - **macOS**: 系统设置 → 通知与专注模式 → StayFocused → 允许通知 - **Windows**: 设置 → 系统 → 通知和操作 → 允许StayFocused发送通知 ### Q: 桌面版与Web版有什么区别? A: 桌面版提供离线使用、系统通知、后台播放等额外功能,且整体性能更好,不受浏览器限制。 ## 🤝 贡献指南 欢迎为StayFocused贡献代码或提供建议!以下是参与项目的方式: 1. Fork本仓库 2. 创建您的特性分支 (`git checkout -b feature/amazing-feature`) 3. 提交您的更改 (`git commit -m 'Add some amazing feature'`) 4. 推送到分支 (`git push origin feature/amazing-feature`) 5. 打开Pull Request ### 贡献类型 - **功能开发**: 添加新功能或改进现有功能 - **Bug修复**: 修复已知问题 - **文档改进**: 完善文档 - **性能优化**: 提高应用性能 - **声音资源**: 贡献高质量声音资源 ## 📝 许可协议 本项目采用 [MIT License](LICENSE) 开源许可协议。 ---

Made with ❤️ by YOUR-NAME

如果StayFocused对您有所帮助,请考虑给项目一个⭐️

================================================ FILE: electron/main.cjs ================================================ const { app, BrowserWindow, globalShortcut, protocol } = require('electron'); const path = require('path'); const fs = require('fs'); const isDev = process.env.NODE_ENV === 'development'; let mainWindow; // Windows平台特定设置 if (process.platform === 'win32') { // 设置应用用户模型ID,用于Windows通知和任务栏分组 app.setAppUserModelId('com.stayfocused.app'); } function createWindow() { // 创建浏览器窗口 mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: true, contextIsolation: false, preload: path.join(__dirname, 'preload.cjs') }, // Windows平台使用标准标题栏,macOS使用无标题栏 titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default', // 仅在macOS上设置交通灯按钮位置 ...(process.platform === 'darwin' ? { trafficLightPosition: { x: 10, y: 10 } } : {}), }); // 加载应用 if (isDev) { mainWindow.loadURL('http://localhost:5173'); // Vite 开发服务器地址 // 开发模式下使用快捷键打开开发者工具 const devToolsShortcut = process.platform === 'darwin' ? 'Command+Option+I' : 'Control+Shift+I'; globalShortcut.register(devToolsShortcut, () => { mainWindow.webContents.toggleDevTools(); }); } else { // 注册自定义协议处理音频文件 protocol.registerFileProtocol('app', (request, callback) => { const url = request.url.substring(6); // 移除 'app://' 前缀 try { // 将请求路径映射到应用目录 let filePath; if (url.startsWith('/sounds/')) { // 对于声音文件使用正确路径 filePath = path.join(__dirname, '../dist', url); } else { filePath = path.normalize(`${__dirname}/../dist/${url}`); } return callback(filePath); } catch (error) { console.error('Protocol error:', error); return callback(404); } }); mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); } // 窗口关闭时退出应用 mainWindow.on('closed', () => { mainWindow = null; }); } // 当 Electron 完成初始化时创建窗口 app.whenReady().then(() => { createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); // 当应用退出时注销所有快捷键 app.on('will-quit', () => { globalShortcut.unregisterAll(); }); ================================================ FILE: electron/preload.cjs ================================================ const { contextBridge, ipcRenderer } = require('electron'); // 在窗口加载完成后执行 window.addEventListener('DOMContentLoaded', () => { // 添加样式,为内容提供足够的上边距 const style = document.createElement('style'); style.textContent = ` body { padding-top: 28px !important; /* 为窗口控制按钮留出空间 */ } /* 创建可拖拽区域 */ .titlebar-drag-region { position: fixed; top: 0; left: 0; width: 100%; height: 28px; -webkit-app-region: drag; z-index: 1000; pointer-events: none; } /* 确保交互元素不受拖拽影响 */ button, select, input, a, [role="button"], [role="link"], [role="menuitem"] { -webkit-app-region: no-drag; pointer-events: auto; } `; document.head.appendChild(style); // 创建拖拽区域元素 const dragRegion = document.createElement('div'); dragRegion.className = 'titlebar-drag-region'; document.body.appendChild(dragRegion); // 暴露 Electron API 到渲染进程 contextBridge.exposeInMainWorld('electron', { // 应用信息 isElectron: true, // 播放器相关API player: { canPlayAudio: true, getAudioPath: (relativePath) => { // 确保路径正确处理 if (relativePath.startsWith('/')) { return `app:/${relativePath}`; } return `app:/${relativePath}`; } }, // 其他可能需要的API appInfo: { version: process.env.npm_package_version || '1.0.0', name: process.env.npm_package_name || 'StayFocused' } }); }); ================================================ FILE: eslint.config.js ================================================ import js from '@eslint/js'; import globals from 'globals'; import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; import tseslint from 'typescript-eslint'; export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, } ); ================================================ FILE: index.html ================================================ Stay Focused - 专注时钟 & 白噪音 | 免费提升专注力工具
================================================ FILE: package.json ================================================ { "name": "stayfocused", "private": true, "version": "1.0.0", "description": "专注时钟 & 白噪音应用", "main": "electron/main.cjs", "type": "module", "engines": { "node": ">=16.0.0" }, "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint .", "preview": "vite preview", "electron:dev": "NODE_ENV=development electron .", "electron:build": "npm run build && electron-builder", "electron:preview": "npm run build && electron .", "dist:mac": "npm run build && electron-builder --mac --arm64", "dist:mac-intel": "npm run build && electron-builder --mac --x64", "dist:win": "npm run build && electron-builder --win --x64", "dist:win-ia32": "npm run build && electron-builder --win --ia32", "generate:win-icon": "node scripts/generate-win-icon.cjs", "dist": "npm run build && electron-builder --mac --arm64" }, "dependencies": { "i18next": "^24.2.0", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.4.0", "zustand": "^4.5.2" }, "devDependencies": { "@eslint/js": "^9.9.1", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.18", "electron": "^35.5.0", "electron-builder": "^24.13.3", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", "postcss": "^8.4.35", "sharp": "^0.34.2", "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.2" }, "build": { "appId": "com.stayfocused.app", "productName": "StayFocused", "copyright": "Copyright © 2024", "mac": { "category": "public.app-category.productivity", "icon": "public/icon.icns", "target": [ "dmg" ], "artifactName": "${productName}-${version}-${arch}.${ext}", "darkModeSupport": true }, "dmg": { "writeUpdateInfo": false }, "win": { "icon": "public/icons-win/icon.ico", "target": [ "nsis" ], "artifactName": "${productName}-${version}-${arch}.${ext}" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, "shortcutName": "StayFocused 专注时钟" }, "linux": { "target": [ "AppImage" ], "icon": "public/icons/icon-512.png" }, "files": [ "dist/**/*", "electron/**/*" ], "directories": { "output": "release" }, "publish": null } } ================================================ FILE: postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: public/robots.txt ================================================ # Stay Focused - 专注时钟 & 白噪音 # https://shutong.work/ User-agent: * Allow: / # 禁止访问的目录 Disallow: /src/ Disallow: /node_modules/ Disallow: /.git/ Disallow: /.vscode/ Disallow: /dist/ Disallow: /build/ # 允许访问的重要文件 Allow: /favicon.svg Allow: /logo.svg Allow: /sounds/ # Sitemap 位置 Sitemap: https://shutong.work/sitemap.xml # 爬取延迟(可选,对于高流量网站) Crawl-delay: 1 # 特定搜索引擎配置 User-agent: Googlebot Allow: / User-agent: Bingbot Allow: / User-agent: Baiduspider Allow: / ================================================ FILE: public/sitemap.xml ================================================ https://shutong.work/ 2024-12-01 weekly 1.0 https://shutong.work/?lang=en 2024-12-01 weekly 0.9 ================================================ FILE: scripts/generate-icons.cjs ================================================ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const { execSync } = require('child_process'); const sizes = [16, 32, 64, 128, 256, 512, 1024]; const svgPath = path.join(__dirname, '../public/focus-icon.svg'); const iconsDirPath = path.join(__dirname, '../public/icons'); const iconsetDirPath = path.join(__dirname, '../public/icon.iconset'); // 确保目录存在 if (!fs.existsSync(iconsDirPath)) { fs.mkdirSync(iconsDirPath, { recursive: true }); } if (!fs.existsSync(iconsetDirPath)) { fs.mkdirSync(iconsetDirPath, { recursive: true }); } // 生成各种尺寸的PNG图标 async function generatePngIcons() { console.log('正在生成PNG图标...'); for (const size of sizes) { const outputPath = path.join(iconsDirPath, `icon-${size}.png`); await sharp(svgPath) .resize(size, size) .png() .toFile(outputPath); console.log(`已生成 ${size}x${size} 图标`); } // 复制到iconset目录用于生成Mac图标 for (const size of sizes) { if (size === 16 || size === 32 || size === 64 || size === 128 || size === 256 || size === 512 || size === 1024) { const sourcePath = path.join(iconsDirPath, `icon-${size}.png`); // 为每个尺寸生成1x和2x版本 if (size <= 512) { const targetPath1x = path.join(iconsetDirPath, `icon_${size/2}x${size/2}.png`); fs.copyFileSync(sourcePath, targetPath1x); console.log(`已复制 ${size}x${size} 到 ${targetPath1x}`); } const targetPath2x = path.join(iconsetDirPath, `icon_${size/2}x${size/2}@2x.png`); fs.copyFileSync(sourcePath, targetPath2x); console.log(`已复制 ${size}x${size} 到 ${targetPath2x}`); } } } // 生成Mac图标 async function generateMacIcon() { console.log('正在生成Mac图标...'); try { // 使用iconutil命令生成icns文件 execSync(`iconutil -c icns "${iconsetDirPath}" -o "${path.join(__dirname, '../public/icon.icns')}"`); console.log('已生成 icon.icns'); } catch (error) { console.error('生成Mac图标失败:', error.message); } } // 更新主PNG图标 async function updateMainPngIcon() { console.log('正在更新主PNG图标...'); await sharp(svgPath) .resize(512, 512) .png() .toFile(path.join(__dirname, '../public/icon.png')); console.log('已更新 icon.png'); } // 执行所有任务 async function main() { try { await generatePngIcons(); await updateMainPngIcon(); await generateMacIcon(); console.log('所有图标生成完成!'); } catch (error) { console.error('图标生成失败:', error); } } main(); ================================================ FILE: scripts/generate-icons.js ================================================ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const { execSync } = require('child_process'); const sizes = [16, 32, 64, 128, 256, 512, 1024]; const svgPath = path.join(__dirname, '../public/focus-icon.svg'); const iconsDirPath = path.join(__dirname, '../public/icons'); const iconsetDirPath = path.join(__dirname, '../public/icon.iconset'); // 确保目录存在 if (!fs.existsSync(iconsDirPath)) { fs.mkdirSync(iconsDirPath, { recursive: true }); } if (!fs.existsSync(iconsetDirPath)) { fs.mkdirSync(iconsetDirPath, { recursive: true }); } // 生成各种尺寸的PNG图标 async function generatePngIcons() { console.log('正在生成PNG图标...'); for (const size of sizes) { const outputPath = path.join(iconsDirPath, `icon-${size}.png`); await sharp(svgPath) .resize(size, size) .png() .toFile(outputPath); console.log(`已生成 ${size}x${size} 图标`); } // 复制到iconset目录用于生成Mac图标 for (const size of sizes) { if (size === 16 || size === 32 || size === 64 || size === 128 || size === 256 || size === 512 || size === 1024) { const sourcePath = path.join(iconsDirPath, `icon-${size}.png`); // 为每个尺寸生成1x和2x版本 if (size <= 512) { const targetPath1x = path.join(iconsetDirPath, `icon_${size/2}x${size/2}.png`); fs.copyFileSync(sourcePath, targetPath1x); console.log(`已复制 ${size}x${size} 到 ${targetPath1x}`); } const targetPath2x = path.join(iconsetDirPath, `icon_${size/2}x${size/2}@2x.png`); fs.copyFileSync(sourcePath, targetPath2x); console.log(`已复制 ${size}x${size} 到 ${targetPath2x}`); } } } // 生成Mac图标 async function generateMacIcon() { console.log('正在生成Mac图标...'); try { // 使用iconutil命令生成icns文件 execSync(`iconutil -c icns "${iconsetDirPath}" -o "${path.join(__dirname, '../public/icon.icns')}"`); console.log('已生成 icon.icns'); } catch (error) { console.error('生成Mac图标失败:', error.message); } } // 更新主PNG图标 async function updateMainPngIcon() { console.log('正在更新主PNG图标...'); await sharp(svgPath) .resize(512, 512) .png() .toFile(path.join(__dirname, '../public/icon.png')); console.log('已更新 icon.png'); } // 执行所有任务 async function main() { try { await generatePngIcons(); await updateMainPngIcon(); await generateMacIcon(); console.log('所有图标生成完成!'); } catch (error) { console.error('图标生成失败:', error); } } main(); ================================================ FILE: scripts/generate-win-icon.cjs ================================================ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const sharp = require('sharp'); const iconsDir = path.join(__dirname, '../public/icons'); const winIconDir = path.join(__dirname, '../public/icons-win'); // 确保目录存在 if (!fs.existsSync(winIconDir)) { fs.mkdirSync(winIconDir, { recursive: true }); } // 确保icons目录中有所需的PNG图标 async function ensureIconsExist() { const sizes = [16, 24, 32, 48, 64, 128, 256]; const svgPath = path.join(__dirname, '../public/focus-icon.svg'); console.log('检查并生成必要的PNG图标...'); for (const size of sizes) { const outputPath = path.join(iconsDir, `icon-${size}.png`); if (!fs.existsSync(outputPath)) { console.log(`生成 ${size}x${size} 图标...`); await sharp(svgPath) .resize(size, size) .png() .toFile(outputPath); } } } // 使用 png-to-ico 工具创建 .ico 文件 async function createIcoWithNpm() { try { console.log('尝试使用png-to-ico创建Windows图标...'); // 检查是否安装了png-to-ico try { execSync('npm list -g png-to-ico'); } catch (error) { console.log('安装png-to-ico工具...'); execSync('npm install -g png-to-ico'); } // 创建图标文件 const iconFiles = [16, 24, 32, 48, 64, 128, 256].map(size => path.join(iconsDir, `icon-${size}.png`) ); const iconFilesArg = iconFiles.join(' '); const outputPath = path.join(winIconDir, 'icon.ico'); execSync(`png-to-ico ${iconFilesArg} > "${outputPath}"`); console.log(`Windows图标已创建: ${outputPath}`); return true; } catch (error) { console.error('使用png-to-ico创建图标失败:', error.message); return false; } } // 使用 ImageMagick 创建 .ico 文件 async function createIcoWithImageMagick() { try { console.log('尝试使用ImageMagick创建Windows图标...'); const inputPath = path.join(iconsDir, 'icon-256.png'); const outputPath = path.join(winIconDir, 'icon.ico'); // 测试ImageMagick是否可用 try { execSync('magick --version'); } catch (error) { console.error('ImageMagick未安装或不可用'); console.log('请安装ImageMagick: https://imagemagick.org/script/download.php'); return false; } execSync(`magick convert "${inputPath}" -define icon:auto-resize=256,128,64,48,32,16 "${outputPath}"`); console.log(`Windows图标已创建: ${outputPath}`); return true; } catch (error) { console.error('使用ImageMagick创建图标失败:', error.message); return false; } } // 主函数 async function main() { try { await ensureIconsExist(); let success = await createIcoWithNpm(); if (!success) { success = await createIcoWithImageMagick(); } if (!success) { console.log('手动创建Windows图标的方法:'); console.log('1. 使用在线转换工具,如 https://convertico.com/'); console.log('2. 上传 public/icons/icon-256.png 文件'); console.log('3. 下载转换后的.ico文件并保存到 public/icons-win/icon.ico'); } console.log('Windows图标生成过程完成'); } catch (error) { console.error('生成Windows图标时出错:', error); } } main(); ================================================ FILE: scripts/seo-check.cjs ================================================ #!/usr/bin/env node /** * SEO 检查脚本 * 用于在构建时验证 SEO 配置的正确性 * * 使用: node scripts/seo-check.cjs */ const fs = require('fs'); const path = require('path'); const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[36m', bold: '\x1b[1m' }; const log = { success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`), error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`), warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`), header: (msg) => console.log(`\n${colors.bold}${colors.blue}${msg}${colors.reset}\n`) }; let errorCount = 0; let warningCount = 0; let checkCount = 0; function checkFile(filePath, description) { checkCount++; if (fs.existsSync(filePath)) { log.success(`${description}: ${filePath}`); return true; } else { log.error(`缺少 ${description}: ${filePath}`); errorCount++; return false; } } function checkFileContent(filePath, pattern, description) { checkCount++; try { const content = fs.readFileSync(filePath, 'utf8'); if (pattern.test ? pattern.test(content) : content.includes(pattern)) { log.success(description); return true; } else { log.warning(description); warningCount++; return false; } } catch (e) { log.error(`无法读取文件 ${filePath}: ${e.message}`); errorCount++; return false; } } console.log(`\n${colors.bold}${colors.blue}🔍 Stay Focused SEO 检查报告${colors.reset}\n`); // 1. 检查必要的文件 log.header('1. 检查必要文件'); checkFile(path.join(__dirname, '../index.html'), 'HTML 入口文件'); checkFile(path.join(__dirname, '../public/sitemap.xml'), 'Sitemap 文件'); checkFile(path.join(__dirname, '../public/robots.txt'), 'Robots 文件'); checkFile(path.join(__dirname, '../src/components/SEOHead.tsx'), 'SEO Head 组件'); checkFile(path.join(__dirname, '../src/hooks/useSEO.ts'), 'useSEO Hook'); checkFile(path.join(__dirname, '../src/utils/seoAudit.ts'), 'SEO 审核工具'); checkFile(path.join(__dirname, '../src/hooks/useWebVitals.ts'), 'Web Vitals Hook'); checkFile(path.join(__dirname, '../src/data/seoConfig.ts'), 'SEO 配置文件'); // 2. 检查 index.html 中的 Meta 标签 log.header('2. 检查 HTML Meta 标签'); const htmlPath = path.join(__dirname, '../index.html'); checkFileContent(htmlPath, 'meta name="description"', 'Meta description 标签'); checkFileContent(htmlPath, 'meta name="keywords"', 'Meta keywords 标签'); checkFileContent(htmlPath, 'meta property="og:title"', 'OG title 标签'); checkFileContent(htmlPath, 'meta property="og:description"', 'OG description 标签'); checkFileContent(htmlPath, 'meta property="og:image"', 'OG image 标签'); checkFileContent(htmlPath, 'meta name="twitter:card"', 'Twitter Card 标签'); checkFileContent(htmlPath, 'link rel="canonical"', 'Canonical 链接'); checkFileContent(htmlPath, 'link rel="alternate" hreflang', 'Hreflang 备用链接'); // 3. 检查结构化数据 log.header('3. 检查结构化数据'); checkFileContent(htmlPath, /type="application\/ld\+json"/, 'JSON-LD 脚本'); checkFileContent(htmlPath, '"@type": "WebApplication"', 'WebApplication Schema'); checkFileContent(htmlPath, '"@type": "FAQPage"', 'FAQPage Schema'); checkFileContent(htmlPath, '"@type": "BreadcrumbList"', 'BreadcrumbList Schema'); // 4. 检查移动端优化 log.header('4. 检查移动端优化'); checkFileContent(htmlPath, 'meta name="viewport"', 'Viewport meta 标签'); checkFileContent(htmlPath, 'meta name="apple-mobile-web-app-capable"', 'iOS web app 支持'); checkFileContent(htmlPath, 'meta name="theme-color"', 'Theme color 标签'); // 5. 检查性能优化 log.header('5. 检查性能优化'); checkFileContent(htmlPath, 'link rel="preconnect"', 'Preconnect 链接'); checkFileContent(htmlPath, 'link rel="dns-prefetch"', 'DNS Prefetch'); checkFileContent(htmlPath, 'link rel="preload"', 'Preload 资源'); // 6. 检查 SEO 配置文件 log.header('6. 检查 SEO 配置内容'); const seoConfigPath = path.join(__dirname, '../src/data/seoConfig.ts'); if (fs.existsSync(seoConfigPath)) { const seoConfig = fs.readFileSync(seoConfigPath, 'utf8'); checkCount++; if (seoConfig.includes('keywordLibrary')) { log.success('SEO 配置包含关键词库'); } else { log.error('SEO 配置缺少关键词库'); errorCount++; } checkCount++; if (seoConfig.includes('faqSchema')) { log.success('SEO 配置包含 FAQ Schema'); } else { log.error('SEO 配置缺少 FAQ Schema'); errorCount++; } checkCount++; if (seoConfig.includes('pageMetadata')) { log.success('SEO 配置包含页面元数据'); } else { log.error('SEO 配置缺少页面元数据'); errorCount++; } } // 7. 检查 robots.txt log.header('7. 检查 Robots.txt'); const robotsPath = path.join(__dirname, '../public/robots.txt'); checkFileContent(robotsPath, 'User-agent: *', 'User-agent 规则'); checkFileContent(robotsPath, 'Sitemap:', 'Sitemap 声明'); checkFileContent(robotsPath, 'Allow:', 'Allow 规则'); // 8. 检查 sitemap.xml log.header('8. 检查 Sitemap.xml'); const sitemapPath = path.join(__dirname, '../public/sitemap.xml'); checkFileContent(sitemapPath, ' 0) { console.log(`${colors.red}❌ SEO 检查失败!请修复上述错误。${colors.reset}\n`); process.exit(1); } else if (warningCount > 0) { console.log(`${colors.yellow}⚠️ SEO 检查通过,但有 ${warningCount} 个警告。${colors.reset}\n`); process.exit(0); } else { console.log(`${colors.green}✅ SEO 检查完全通过!${colors.reset}\n`); process.exit(0); } ================================================ FILE: scripts/seo-check.js ================================================ #!/usr/bin/env node /** * SEO 检查脚本 * 用于在构建时验证 SEO 配置的正确性 * * 使用: node scripts/seo-check.js */ const fs = require('fs'); const path = require('path'); const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[36m', bold: '\x1b[1m' }; const log = { success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`), error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`), warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`), header: (msg) => console.log(`\n${colors.bold}${colors.blue}${msg}${colors.reset}\n`) }; let errorCount = 0; let warningCount = 0; let checkCount = 0; function checkFile(filePath, description) { checkCount++; if (fs.existsSync(filePath)) { log.success(`${description}: ${filePath}`); return true; } else { log.error(`缺少 ${description}: ${filePath}`); errorCount++; return false; } } function checkFileContent(filePath, pattern, description) { checkCount++; try { const content = fs.readFileSync(filePath, 'utf8'); if (pattern.test ? pattern.test(content) : content.includes(pattern)) { log.success(description); return true; } else { log.warning(description); warningCount++; return false; } } catch (e) { log.error(`无法读取文件 ${filePath}: ${e.message}`); errorCount++; return false; } } console.log(`\n${colors.bold}${colors.blue}🔍 Stay Focused SEO 检查报告${colors.reset}\n`); // 1. 检查必要的文件 log.header('1. 检查必要文件'); checkFile(path.join(__dirname, '../index.html'), 'HTML 入口文件'); checkFile(path.join(__dirname, '../public/sitemap.xml'), 'Sitemap 文件'); checkFile(path.join(__dirname, '../public/robots.txt'), 'Robots 文件'); checkFile(path.join(__dirname, '../src/components/SEOHead.tsx'), 'SEO Head 组件'); checkFile(path.join(__dirname, '../src/hooks/useSEO.ts'), 'useSEO Hook'); checkFile(path.join(__dirname, '../src/utils/seoAudit.ts'), 'SEO 审核工具'); checkFile(path.join(__dirname, '../src/hooks/useWebVitals.ts'), 'Web Vitals Hook'); checkFile(path.join(__dirname, '../src/data/seoConfig.ts'), 'SEO 配置文件'); // 2. 检查 index.html 中的 Meta 标签 log.header('2. 检查 HTML Meta 标签'); const htmlPath = path.join(__dirname, '../index.html'); checkFileContent(htmlPath, 'meta name="description"', 'Meta description 标签'); checkFileContent(htmlPath, 'meta name="keywords"', 'Meta keywords 标签'); checkFileContent(htmlPath, 'meta property="og:title"', 'OG title 标签'); checkFileContent(htmlPath, 'meta property="og:description"', 'OG description 标签'); checkFileContent(htmlPath, 'meta property="og:image"', 'OG image 标签'); checkFileContent(htmlPath, 'meta name="twitter:card"', 'Twitter Card 标签'); checkFileContent(htmlPath, 'link rel="canonical"', 'Canonical 链接'); checkFileContent(htmlPath, 'link rel="alternate" hreflang', 'Hreflang 备用链接'); // 3. 检查结构化数据 log.header('3. 检查结构化数据'); checkFileContent(htmlPath, /type="application\/ld\+json"/, 'JSON-LD 脚本'); checkFileContent(htmlPath, '"@type": "WebApplication"', 'WebApplication Schema'); checkFileContent(htmlPath, '"@type": "FAQPage"', 'FAQPage Schema'); checkFileContent(htmlPath, '"@type": "BreadcrumbList"', 'BreadcrumbList Schema'); // 4. 检查移动端优化 log.header('4. 检查移动端优化'); checkFileContent(htmlPath, 'meta name="viewport"', 'Viewport meta 标签'); checkFileContent(htmlPath, 'meta name="apple-mobile-web-app-capable"', 'iOS web app 支持'); checkFileContent(htmlPath, 'meta name="theme-color"', 'Theme color 标签'); // 5. 检查性能优化 log.header('5. 检查性能优化'); checkFileContent(htmlPath, 'link rel="preconnect"', 'Preconnect 链接'); checkFileContent(htmlPath, 'link rel="dns-prefetch"', 'DNS Prefetch'); checkFileContent(htmlPath, 'link rel="preload"', 'Preload 资源'); // 6. 检查 SEO 配置文件 log.header('6. 检查 SEO 配置内容'); const seoConfigPath = path.join(__dirname, '../src/data/seoConfig.ts'); if (fs.existsSync(seoConfigPath)) { const seoConfig = fs.readFileSync(seoConfigPath, 'utf8'); checkCount++; if (seoConfig.includes('keywordLibrary')) { log.success('SEO 配置包含关键词库'); } else { log.error('SEO 配置缺少关键词库'); errorCount++; } checkCount++; if (seoConfig.includes('faqSchema')) { log.success('SEO 配置包含 FAQ Schema'); } else { log.error('SEO 配置缺少 FAQ Schema'); errorCount++; } checkCount++; if (seoConfig.includes('pageMetadata')) { log.success('SEO 配置包含页面元数据'); } else { log.error('SEO 配置缺少页面元数据'); errorCount++; } } // 7. 检查 robots.txt log.header('7. 检查 Robots.txt'); const robotsPath = path.join(__dirname, '../public/robots.txt'); checkFileContent(robotsPath, 'User-agent: *', 'User-agent 规则'); checkFileContent(robotsPath, 'Sitemap:', 'Sitemap 声明'); checkFileContent(robotsPath, 'Allow:', 'Allow 规则'); // 8. 检查 sitemap.xml log.header('8. 检查 Sitemap.xml'); const sitemapPath = path.join(__dirname, '../public/sitemap.xml'); checkFileContent(sitemapPath, ' 0) { console.log(`${colors.red}❌ SEO 检查失败!请修复上述错误。${colors.reset}\n`); process.exit(1); } else if (warningCount > 0) { console.log(`${colors.yellow}⚠️ SEO 检查通过,但有 ${warningCount} 个警告。${colors.reset}\n`); process.exit(0); } else { console.log(`${colors.green}✅ SEO 检查完全通过!${colors.reset}\n`); process.exit(0); } ================================================ FILE: src/App.tsx ================================================ import React, { useEffect, useState } from 'react'; import { Layout } from './components/Layout'; import { Timer } from './components/Timer'; import { SoundButton } from './components/SoundButton'; import { SavedMixes } from './components/SavedMixes'; import { useThemeStore } from './store/useThemeStore'; import { useSEO } from './hooks/useSEO'; import { useWebVitals } from './hooks/useWebVitals'; import { initAnalytics } from './utils/analytics'; import { initSEOAudit } from './utils/seoAudit'; import { sounds } from './data/sounds'; import { categoryToKey } from './data/categoryMap'; import { useTranslation } from 'react-i18next'; function App() { const { getTheme } = useThemeStore(); const theme = getTheme(); const { t, ready } = useTranslation(); const [isLoading, setIsLoading] = useState(true); // 使用 SEO Hook 来管理动态 SEO 标签 useSEO(); // 监控 Web Vitals useWebVitals( (metrics) => { if (import.meta.env.DEV) { console.log('Web Vitals Metrics:', metrics); } }, import.meta.env.DEV ); useEffect(() => { if (ready) { setIsLoading(false); } }, [ready]); // 初始化分析工具 useEffect(() => { initAnalytics(); }, []); // 初始化 SEO 审核 useEffect(() => { initSEOAudit(); }, []); if (isLoading) { return (
Loading...
); } return (
{/* 左侧声音类别 */}
{Array.from(new Set(sounds.map(sound => sound.category))).map(category => (

{t(`categories.${categoryToKey[category]}`)}

{sounds .filter(sound => sound.category === category) .map(sound => ( ))}
))}
{/* 右侧控制面板 */}
{/* 专注时间 */} {/* 我的混音 */}

{t('mixes.title')}

); } export default App; ================================================ FILE: src/components/AlertDialog.tsx ================================================ import React from 'react'; import { X } from 'lucide-react'; import { useThemeStore } from '../store/useThemeStore'; interface AlertDialogProps { isOpen: boolean; onClose: () => void; title: string; message: string; } export const AlertDialog: React.FC = ({ isOpen, onClose, title, message, }) => { const { getTheme } = useThemeStore(); const theme = getTheme(); if (!isOpen) return null; return (

{title}

{message}

); }; ================================================ FILE: src/components/LanguageSwitcher.tsx ================================================ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Languages } from 'lucide-react'; import { useThemeStore } from '../store/useThemeStore'; export const LanguageSwitcher: React.FC = () => { const { i18n } = useTranslation(); const { getTheme } = useThemeStore(); const theme = getTheme(); const currentLanguage = i18n.language; const toggleLanguage = () => { const newLanguage = currentLanguage.startsWith('zh') ? 'en' : 'zh'; i18n.changeLanguage(newLanguage).then(() => { localStorage.setItem('preferred-language', newLanguage); // 强制所有组件重新渲染 window.dispatchEvent(new Event('languageChanged')); }); }; return ( ); }; ================================================ FILE: src/components/Layout.tsx ================================================ import React from 'react'; import { useThemeStore } from '../store/useThemeStore'; import { ThemeToggle } from './ThemeToggle'; import { LanguageSwitcher } from './LanguageSwitcher'; import { Brain } from 'lucide-react'; import { useTranslation } from 'react-i18next'; interface LayoutProps { children: React.ReactNode; } export const Layout: React.FC = ({ children }) => { const { getTheme } = useThemeStore(); const theme = getTheme(); const { t } = useTranslation(); return (
{/* 导航栏 */} {/* 主要内容 */}
{children}
{/* 页脚 */}
); }; ================================================ FILE: src/components/SEOHead.tsx ================================================ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; interface SEOHeadProps { title?: string; description?: string; keywords?: string; canonical?: string; noindex?: boolean; ogImage?: string; structuredData?: object; } export const SEOHead: React.FC = ({ title, description, keywords, canonical, noindex = false, ogImage, structuredData }) => { const { i18n } = useTranslation(); useEffect(() => { // 更新页面标题 if (title) { document.title = title; } // 更新描述 if (description) { let descriptionMeta = document.querySelector('meta[name="description"]'); if (!descriptionMeta) { descriptionMeta = document.createElement('meta'); descriptionMeta.setAttribute('name', 'description'); document.head.appendChild(descriptionMeta); } descriptionMeta.setAttribute('content', description); } // 更新关键词 if (keywords) { let keywordsMeta = document.querySelector('meta[name="keywords"]'); if (!keywordsMeta) { keywordsMeta = document.createElement('meta'); keywordsMeta.setAttribute('name', 'keywords'); document.head.appendChild(keywordsMeta); } keywordsMeta.setAttribute('content', keywords); } // 更新canonical链接 if (canonical) { let canonicalLink = document.querySelector('link[rel="canonical"]'); if (!canonicalLink) { canonicalLink = document.createElement('link'); canonicalLink.setAttribute('rel', 'canonical'); document.head.appendChild(canonicalLink); } canonicalLink.setAttribute('href', canonical); } // 设置robots meta let robotsMeta = document.querySelector('meta[name="robots"]'); if (!robotsMeta) { robotsMeta = document.createElement('meta'); robotsMeta.setAttribute('name', 'robots'); document.head.appendChild(robotsMeta); } robotsMeta.setAttribute('content', noindex ? 'noindex, nofollow' : 'index, follow'); // 更新Open Graph图片 if (ogImage) { let ogImageMeta = document.querySelector('meta[property="og:image"]'); if (!ogImageMeta) { ogImageMeta = document.createElement('meta'); ogImageMeta.setAttribute('property', 'og:image'); document.head.appendChild(ogImageMeta); } ogImageMeta.setAttribute('content', ogImage); // 同时更新Twitter Card图片 let twitterImageMeta = document.querySelector('meta[name="twitter:image"]'); if (!twitterImageMeta) { twitterImageMeta = document.createElement('meta'); twitterImageMeta.setAttribute('name', 'twitter:image'); document.head.appendChild(twitterImageMeta); } twitterImageMeta.setAttribute('content', ogImage); } // 添加结构化数据 if (structuredData) { let structuredDataScript = document.querySelector('script[type="application/ld+json"][data-seo-head]'); if (!structuredDataScript) { structuredDataScript = document.createElement('script'); structuredDataScript.setAttribute('type', 'application/ld+json'); structuredDataScript.setAttribute('data-seo-head', 'true'); document.head.appendChild(structuredDataScript); } structuredDataScript.textContent = JSON.stringify(structuredData, null, 2); } // 清理函数 return () => { // 在组件卸载时可以选择性地清理某些meta标签 if (noindex) { const robotsMeta = document.querySelector('meta[name="robots"]'); if (robotsMeta) { robotsMeta.setAttribute('content', 'index, follow'); } } }; }, [title, description, keywords, canonical, noindex, ogImage, structuredData, i18n.language]); return null; // 这个组件不渲染任何内容 }; // 预定义的SEO配置 export const SEOConfigs = { home: { zh: { title: 'Stay Focused - 专注时钟 & 白噪音 | 提升专注力的在线工具', description: 'Stay Focused是一款免费的在线专注时钟和白噪音应用,提供多种自然声音如雨声、森林鸟鸣、海浪等,帮助您提升专注力、提高工作效率、改善睡眠质量。支持自定义混音、专注计时器和多种主题。', keywords: '专注时钟,白噪音,专注力,工作效率,番茄钟,自然声音,雨声,森林,海浪,专注音乐,在线工具,免费应用' }, en: { title: 'Stay Focused - Focus Timer & White Noise | Boost Your Productivity', description: 'Stay Focused is a free online focus timer and white noise app with various natural sounds like rain, forest birds, ocean waves to help you boost concentration, improve productivity and enhance sleep quality. Features custom sound mixing, focus timer and multiple themes.', keywords: 'focus timer,white noise,concentration,productivity,pomodoro,natural sounds,rain,forest,ocean waves,focus music,online tool,free app' } }, timer: { zh: { title: '专注计时器 - Stay Focused | 番茄工作法计时器', description: '使用Stay Focused的专注计时器,采用番茄工作法提升工作效率。配合白噪音和自然声音,创造完美的专注环境。', keywords: '专注计时器,番茄工作法,番茄钟,工作效率,时间管理,专注训练' }, en: { title: 'Focus Timer - Stay Focused | Pomodoro Technique Timer', description: 'Use Stay Focused focus timer with Pomodoro Technique to boost productivity. Combined with white noise and natural sounds for perfect focus environment.', keywords: 'focus timer,pomodoro technique,pomodoro timer,productivity,time management,focus training' } } }; ================================================ FILE: src/components/SavedMixes.tsx ================================================ import React, { useState } from 'react'; import { Save, List, Music, Trash2 } from 'lucide-react'; import { useStore } from '../store/useStore'; import { useThemeStore } from '../store/useThemeStore'; import { useTranslation } from 'react-i18next'; export const SavedMixes: React.FC = () => { const { savedMixes, saveMix, loadMix, deleteMix } = useStore(); const { getTheme } = useThemeStore(); const theme = getTheme(); const { t } = useTranslation(); const [newMixName, setNewMixName] = useState(''); const [isNaming, setIsNaming] = useState(false); const [selectedMixIndex, setSelectedMixIndex] = useState(null); const handleSave = () => { if (newMixName.trim()) { saveMix(newMixName); setNewMixName(''); setIsNaming(false); } }; const handleMixSelect = (mix) => { loadMix(mix); setSelectedMixIndex(savedMixes.indexOf(mix)); }; const handleMixDelete = (index) => { deleteMix(index); setSelectedMixIndex(null); }; return (
setNewMixName(e.target.value)} placeholder={t('mixes.namePlaceholder')} className={`w-full px-3 py-2 rounded-lg ${theme.colors.secondary} placeholder:${theme.colors.textSecondary} focus:outline-none focus:ring-2 focus:${theme.colors.accent.replace('text-', 'ring-')} transition-shadow duration-200`} />
{savedMixes.length > 0 && (

{t('mixes.saved')}

{savedMixes.map((mix, index) => (
handleMixSelect(mix)} className={` group p-3 rounded-lg cursor-pointer transition-all duration-300 ease-in-out ${selectedMixIndex === index ? `${theme.colors.accent} bg-opacity-10 ring-2 ring-offset-2 ${theme.colors.accent.replace('text-', 'ring-')} scale-[1.02]` : `${theme.colors.secondary} hover:bg-opacity-70` } `} >

{mix.name || t('mixes.mix') + ` ${index + 1}`}

{mix.volumes && Object.entries(mix.volumes) .filter(([_, volume]) => volume > 0) .map(([soundId]) => t(`sounds.${soundId}`)) .join(' · ') || t('mixes.noSounds')}

))}
)}
); }; ================================================ FILE: src/components/SoundButton.tsx ================================================ import React from 'react'; import { Sound } from '../types'; import { useStore } from '../store/useStore'; import { useThemeStore } from '../store/useThemeStore'; import { SoundIcon } from './SoundIcon'; import { useTranslation } from 'react-i18next'; interface Props { sound: Sound; } export const SoundButton: React.FC = ({ sound }) => { const { activeSounds, addSound, removeSound, setVolume } = useStore(); const { getTheme } = useThemeStore(); const theme = getTheme(); const { t } = useTranslation(); const isPlaying = activeSounds.has(sound.id); const volume = activeSounds.get(sound.id)?.volume || 0.5; const toggleSound = () => { if (isPlaying) { removeSound(sound.id); } else { addSound(sound); } }; return (

{t(`sounds.${sound.id}`)}

{isPlaying && ( setVolume(sound.id, parseFloat(e.target.value))} className="w-24" /> )}
); }; ================================================ FILE: src/components/SoundIcon.tsx ================================================ import React from 'react'; import { Wind, Cloud, CloudRain, Waves, Trees, Bird, Music, Coffee, Umbrella, Leaf, Droplets, CloudLightning, Mountain, Flame, Sun, Moon, Keyboard, Train, Building2, Utensils, TreePine, Store, Bug } from 'lucide-react'; export type IconType = | 'rain-light' | 'rain-heavy' | 'rain-roof' | 'rain-window' | 'thunder' | 'rain-umbrella' | 'rain-leaves' | 'rain-puddle' | 'rain-distant' | 'forest' | 'waves' | 'creek' | 'wind' | 'leaves' | 'waterfall' | 'fire' | 'beach' | 'night-forest' | 'traffic' | 'cafe' | 'keyboard' | 'subway' | 'office' | 'restaurant' | 'park' | 'market' | 'train' | 'birds' | 'crickets' | 'frogs' | 'seagulls' | 'wolves' | 'owls' | 'cats' | 'dolphins' | 'whales'; interface SoundIconProps { type: IconType; size?: number; } export const SoundIcon: React.FC = ({ type, size = 24 }) => { const icons = { 'rain-light': CloudRain, 'rain-heavy': Cloud, 'rain-roof': CloudRain, 'rain-window': CloudRain, 'thunder': CloudLightning, 'rain-umbrella': Umbrella, 'rain-leaves': Leaf, 'rain-puddle': Droplets, 'rain-distant': Cloud, 'forest': Trees, 'waves': Waves, 'creek': Wind, 'wind': Wind, 'leaves': Leaf, 'waterfall': Mountain, 'fire': Flame, 'beach': Sun, 'night-forest': Moon, 'traffic': Music, 'cafe': Coffee, 'keyboard': Keyboard, 'subway': Train, 'office': Building2, 'restaurant': Utensils, 'park': TreePine, 'market': Store, 'train': Train, 'birds': Bird, 'crickets': Bug, 'frogs': Bug, 'seagulls': Bird, 'wolves': Music, 'owls': Moon, 'cats': Music, 'dolphins': Music, 'whales': Music }; const IconComponent = icons[type]; return ; }; ================================================ FILE: src/components/ThemeSelector.tsx ================================================ import React from 'react'; import { useThemeStore } from '../store/useThemeStore'; import { themes } from '../themes'; import { Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; export const ThemeSelector: React.FC = () => { const { currentThemeId, setTheme, getTheme } = useThemeStore(); const currentTheme = getTheme(); const { t } = useTranslation(); return (

{t('themes.title')}

{themes.map((theme) => ( ))}
); }; ================================================ FILE: src/components/ThemeToggle.tsx ================================================ import React, { useState, useRef } from 'react'; import { Palette } from 'lucide-react'; import { useThemeStore } from '../store/useThemeStore'; import { themes } from '../themes'; import { useOnClickOutside } from '../hooks/useOnClickOutside'; import { useTranslation } from 'react-i18next'; export const ThemeToggle: React.FC = () => { const { currentThemeId, setTheme, getTheme } = useThemeStore(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const currentTheme = getTheme(); const { t } = useTranslation(); useOnClickOutside(dropdownRef, () => setIsOpen(false)); return (
{isOpen && (
{themes.map((theme) => ( ))}
)}
); }; ================================================ FILE: src/components/Timer.tsx ================================================ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Play, Square, RotateCcw, Bell, BellOff } from 'lucide-react'; import { useThemeStore } from '../store/useThemeStore'; import { useTimerStore } from '../store/timerStore'; import { AlertDialog } from './AlertDialog'; import { useTranslation } from 'react-i18next'; const TIME_PRESETS = [ { minutes: 25, label: '25' }, { minutes: 45, label: '45' }, { minutes: 60, label: '60' } ]; export const Timer: React.FC = () => { const { t } = useTranslation(); // 从 store 获取状态和动作 const { minutes, isRunning, soundEnabled, actions } = useTimerStore(); const { getTheme } = useThemeStore(); const theme = getTheme(); // 本地状态 const [timeLeft, setTimeLeft] = useState(minutes * 60); const [showAlert, setShowAlert] = useState(false); const [isEditing, setIsEditing] = useState(false); const [editingValue, setEditingValue] = useState(minutes.toString()); const audioRef = useRef(null); // 初始化音频 useEffect(() => { audioRef.current = new Audio('/sounds/bell.mp3'); audioRef.current.preload = 'auto'; }, []); // 播放声音 const playSound = useCallback(() => { if (soundEnabled && audioRef.current) { audioRef.current.play().catch(error => { console.error("Audio play failed:", error); }); } }, [soundEnabled]); // 计时器完成处理 const handleTimerComplete = useCallback(() => { actions.stop(); playSound(); setShowAlert(true); }, [actions, playSound]); // 计时器逻辑 useEffect(() => { let interval: NodeJS.Timeout; if (isRunning && timeLeft > 0) { interval = setInterval(() => { setTimeLeft((prev) => { if (prev <= 1) { handleTimerComplete(); return 0; } return prev - 1; }); }, 1000); } return () => clearInterval(interval); }, [isRunning, timeLeft, handleTimerComplete]); // 监听 minutes 变化 useEffect(() => { if (!isRunning) { setTimeLeft(minutes * 60); setEditingValue(minutes.toString()); } }, [minutes, isRunning]); // 重置计时器 const handleReset = () => { actions.reset(); setTimeLeft(minutes * 60); setShowAlert(false); }; // 处理时间变更 const handleTimeChange = (newMinutes: number) => { if (!isRunning && newMinutes >= 1 && newMinutes <= 999) { actions.setMinutes(newMinutes); } }; // 处理自定义时间输入 const handleCustomTimeSubmit = () => { const newMinutes = Math.min(999, Math.max(1, parseInt(editingValue) || 25)); handleTimeChange(newMinutes); setIsEditing(false); }; // 格式化时间显示 const formatTime = (seconds: number) => { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; // 计算进度 const progress = ((minutes * 60 - timeLeft) / (minutes * 60)) * 100; const progressColor = progress >= 75 ? 'text-red-500' : progress >= 50 ? 'text-yellow-500' : 'text-green-500'; return (
{/* 标题 */}

{t('timer.title')}

{/* 声音开关 */}
{/* 时间显示 */}
{isEditing ? (
setEditingValue(e.target.value)} onBlur={() => { handleCustomTimeSubmit(); setIsEditing(false); }} onKeyDown={(e) => { if (e.key === 'Enter') { handleCustomTimeSubmit(); setIsEditing(false); } else if (e.key === 'Escape') { setIsEditing(false); setEditingValue(minutes.toString()); } }} className={`w-20 text-3xl text-center bg-transparent border-b-2 ${theme.colors.border} focus:outline-none font-mono ${theme.colors.text}`} min="1" max="999" autoFocus /> 分钟
) : (
!isRunning && setIsEditing(true)} className={`text-4xl tracking-wider cursor-pointer ${timeLeft === 0 ? 'text-red-500' : isRunning ? theme.colors.accent : theme.colors.accent + ' opacity-80'} transition-colors duration-300 ${!isRunning && 'hover:opacity-100'}`} style={{ textShadow: isRunning ? '0 0 10px rgba(var(--accent-rgb), 0.5)' : 'none' }} > {formatTime(timeLeft)}
)}
{/* 进度环 */} {/* 发光效果 */} {isRunning && ( )} {/* 背景环 */} {/* 进度环 */} {/* 发光滤镜 */} {/* 刻度 */} {Array.from({ length: 60 }).map((_, i) => { const rotation = i * 6; const isMajor = i % 5 === 0; return ( ); })}
{/* 预设时间按钮 */}
{TIME_PRESETS.map(({ minutes: mins, label }) => ( ))}
{/* 控制按钮 */}
{!isRunning ? ( ) : ( )}
{/* 完成提示 */} { setShowAlert(false); handleReset(); }} title={t('timer.completeTitle')} message={t('timer.completeMessage')} />
); }; ================================================ FILE: src/data/categories.ts ================================================ export const categories = [ { id: 'rain', name: 'Rain', icon: '🌧️' }, { id: 'nature', name: 'Nature', icon: '🌳' }, { id: 'city', name: 'City', icon: '🌆' }, { id: 'animals', name: 'Animals', icon: '🐾' } ] as const; ================================================ FILE: src/data/categoryMap.ts ================================================ export const categoryToKey: Record = { '自然': 'nature', '雨声': 'rain', '城市': 'city', '动物': 'animals' }; ================================================ FILE: src/data/seoConfig.ts ================================================ /** * SEO 配置文件 - 关键词库、元数据、结构化数据 */ // 关键词库 - 按优先级和类别组织 export const keywordLibrary = { zh: { primary: ['专注时钟', '白噪音', '专注力', '番茄钟', '工作效率'], longTail: [ '在线专注时钟', '免费白噪音应用', '提升专注力工具', '番茄工作法计时器', '办公室白噪音', '学习集中力应用', '睡眠白噪音', '冥想音乐应用', '自然声音生成器', '焦点管理工具' ], sounds: ['雨声', '森林鸟鸣', '海浪声', '城市噪音', '雷声', '风声', '流水声', '动物叫声'], features: ['自定义混音', '多种主题', '计时器', '多语言支持', '响应式设计', '离线使用'], useCase: [ '工作专注', '学习复习', '深度思考', '冥想放松', '改善睡眠', '瑜伽练习', '编程工作', '写作创作', '考试备考', '午休放松' ] }, en: { primary: ['focus timer', 'white noise', 'concentration', 'pomodoro', 'productivity'], longTail: [ 'online focus timer', 'free white noise app', 'productivity tool', 'pomodoro technique timer', 'office white noise', 'study concentration app', 'sleep white noise', 'meditation music app', 'nature sound generator', 'focus management tool' ], sounds: ['rain sound', 'forest birds', 'ocean waves', 'city noise', 'thunder', 'wind', 'water stream', 'animal sounds'], features: ['custom mix', 'multiple themes', 'timer', 'multi-language', 'responsive design', 'offline mode'], useCase: [ 'work focus', 'study review', 'deep thinking', 'meditation relaxation', 'sleep improvement', 'yoga practice', 'programming work', 'creative writing', 'exam preparation', 'midday relaxation' ] } }; // 常见问题 Schema 数据 export const faqSchema = { zh: [ { question: '如何使用 Stay Focused 提升专注力?', answer: '选择您喜欢的白噪音背景声,设置专注时间(25/45/60分钟),然后开始工作。白噪音会屏蔽外界干扰,帮助您进入专注状态。' }, { question: 'Stay Focused 支持哪些白噪音?', answer: '我们提供40+种白噪音,包括自然声音(雨声、森林鸟鸣、海浪)、城市噪音、动物叫声等。您还可以自定义混合多种声音。' }, { question: '是否可以保存我的混音方案?', answer: '是的!您可以创建和保存自己的声音混合方案,下次使用时可以直接加载。所有数据都存储在您的本地浏览器中。' }, { question: 'Stay Focused 完全免费吗?', answer: '是的,Stay Focused 完全免费,无需注册,无广告打扰。所有功能都可以免费使用。' }, { question: '可以离线使用吗?', answer: '支持!一旦加载完成,您可以离线使用 Stay Focused。所有声音文件都会被缓存。' }, { question: '支持哪些语言?', answer: '目前支持中文和英文。我们计划在未来添加更多语言版本。' } ], en: [ { question: 'How do I use Stay Focused to improve concentration?', answer: 'Select your preferred white noise background sound, set your focus time (25/45/60 minutes), and start working. The white noise will mask outside distractions and help you enter a focused state.' }, { question: 'What white noise options are available in Stay Focused?', answer: 'We provide 40+ white noise sounds including natural sounds (rain, forest birds, ocean waves), city noise, animal sounds, and more. You can also create custom sound mixes.' }, { question: 'Can I save my sound mix settings?', answer: 'Yes! You can create and save your custom sound mixes. Your saved mixes will be available next time you visit. All data is stored locally in your browser.' }, { question: 'Is Stay Focused completely free?', answer: 'Yes, Stay Focused is completely free with no registration required and no ads. All features are available at no cost.' }, { question: 'Can I use it offline?', answer: 'Yes! Once loaded, you can use Stay Focused offline. All sound files are cached for offline access.' }, { question: 'What languages are supported?', answer: 'Currently, we support Chinese and English. We plan to add more languages in the future.' } ] }; // SEO 元数据配置 export const seoMetadata = { zh: { siteName: 'Stay Focused', siteDescription: 'Stay Focused 是专业的在线专注时钟和白噪音应用', locale: 'zh_CN', alternateLocale: 'en_US', twitterHandle: '@stayfocused', authorName: 'Stay Focused Team', organizationName: 'Stay Focused', organizationLogo: 'https://shutong.work/logo.svg', organizationUrl: 'https://shutong.work/' }, en: { siteName: 'Stay Focused', siteDescription: 'Stay Focused is a professional online focus timer and white noise app', locale: 'en_US', alternateLocale: 'zh_CN', twitterHandle: '@stayfocused', authorName: 'Stay Focused Team', organizationName: 'Stay Focused', organizationLogo: 'https://shutong.work/logo.svg', organizationUrl: 'https://shutong.work/' } }; // 页面路由的 SEO 配置 export const pageMetadata = { home: { zh: { title: 'Stay Focused - 专注时钟 & 白噪音 | 免费提升专注力工具', description: 'Stay Focused是一款免费的在线专注时钟和白噪音应用,提供40+自然声音和城市噪音,支持自定义混音、番茄工作法计时器、多种主题。帮助您提升工作效率、改善睡眠、深度冥想。无需注册,完全免费!', keywords: '专注时钟,白噪音,专注力,番茄钟,工作效率,自然声音,雨声,森林,海浪,专注音乐,在线工具,免费应用,浏览器应用,离线使用', ogImage: 'https://shutong.work/og-image-zh.png', canonical: 'https://shutong.work/' }, en: { title: 'Stay Focused - Focus Timer & White Noise | Free Productivity Booster', description: 'Stay Focused is a free online focus timer and white noise app with 40+ natural sounds. Features Pomodoro timer, custom sound mixing, multiple themes, and offline mode. Perfect for work, study, meditation, and better sleep. No signup required!', keywords: 'focus timer,white noise,concentration,pomodoro,productivity,natural sounds,rain,forest,ocean waves,focus music,online tool,free app,browser app,offline mode', ogImage: 'https://shutong.work/og-image-en.png', canonical: 'https://shutong.work/' } } }; // 应用功能特性的结构化数据 export const applicationFeatures = { zh: [ '40+ 种白噪音和自然声音', '自定义声音混音器', '灵活的专注计时器(25/45/60分钟)', '保存和加载混音方案', '5+ 种应用主题', '多语言支持(中文/英文)', '响应式设计,完美适配移动设备', '完全免费,无广告', '隐私优先,本地数据存储', '离线模式支持', '键盘快捷键支持', '音量控制和音质优化' ], en: [ '40+ white noise and nature sounds', 'Custom sound mixer', 'Flexible focus timer (25/45/60 minutes)', 'Save and load sound mixes', '5+ application themes', 'Multi-language support (Chinese/English)', 'Responsive design for mobile and desktop', 'Completely free with no ads', 'Privacy-first with local data storage', 'Offline mode support', 'Keyboard shortcuts', 'Volume control and audio optimization' ] }; // 结构化数据中的聚合评分 export const ratingData = { ratingValue: '4.8', ratingCount: '250', bestRating: '5', worstRating: '1' }; // 社交媒体分享文案 export const socialShareText = { zh: { twitter: '🎵 Stay Focused - 用白噪音和专注时钟提升工作效率!40+种自然声音,免费无广告。立即尝试 → https://shutong.work', facebook: '💼 Stay Focused:专业的在线专注工具。包含专注计时器、白噪音应用、多种主题。无需注册,完全免费!', linkedin: '🚀 推荐一个提升专注力的工具:Stay Focused。在线专注时钟 + 白噪音应用,帮助你进入深度工作状态。' }, en: { twitter: '🎵 Stay Focused - Boost your productivity with white noise & focus timer! 40+ nature sounds, free & ad-free. Try now → https://shutong.work', facebook: '💼 Stay Focused: Your professional focus tool online. Focus timer, white noise, multiple themes. Free, no signup required!', linkedin: '🚀 Check out Stay Focused - an online focus timer & white noise app that helps you enter deep work mode.' } }; ================================================ FILE: src/data/sounds/animals.ts ================================================ export const animalSounds = [ { id: 'crickets', name: 'Crickets', category: 'animals', icon: '🦗', audioUrl: '/sounds/crickets.mp3' }, { id: 'birds', name: 'Birds', category: 'animals', icon: '🐦', audioUrl: '/sounds/birds.mp3' }, { id: 'seagulls', name: 'Seagulls', category: 'animals', icon: '🕊️', audioUrl: '/sounds/seagulls.mp3' } ]; ================================================ FILE: src/data/sounds/city.ts ================================================ export const citySounds = [ { id: 'city-traffic', name: 'City Traffic', category: 'city', icon: '🌆', audioUrl: '/sounds/city-traffic.mp3' }, { id: 'cafe', name: 'Cafe', category: 'city', icon: '☕', audioUrl: '/sounds/cafe.mp3' }, { id: 'keyboard', name: 'Keyboard', category: 'city', icon: '⌨️', audioUrl: '/sounds/keyboard.mp3' } ]; ================================================ FILE: src/data/sounds/index.ts ================================================ import { rainSounds } from './rain'; import { natureSounds } from './nature'; import { citySounds } from './city'; import { animalSounds } from './animals'; export const sounds = [ ...rainSounds, ...natureSounds, ...citySounds, ...animalSounds ]; ================================================ FILE: src/data/sounds/nature.ts ================================================ export const natureSounds = [ { id: 'forest-birds', name: 'Forest Birds', category: 'nature', icon: '🌳', audioUrl: '/sounds/forest-birds.mp3' }, { id: 'waves', name: 'Ocean Waves', category: 'nature', icon: '🌊', audioUrl: '/sounds/waves.mp3' }, { id: 'creek', name: 'Creek', category: 'nature', icon: '💧', audioUrl: '/sounds/creek.mp3' } ]; ================================================ FILE: src/data/sounds/rain.ts ================================================ export const rainSounds = [ { id: 'rain-light', name: 'Light Rain', category: 'rain', icon: '🌧️', audioUrl: '/sounds/rain-light.mp3' }, { id: 'rain-heavy', name: 'Heavy Rain', category: 'rain', icon: '⛈️', audioUrl: '/sounds/rain-heavy.mp3' }, { id: 'rain-roof', name: 'Rain on Roof', category: 'rain', icon: '🏠', audioUrl: '/sounds/rain-roof.mp3' } ]; ================================================ FILE: src/data/sounds.ts ================================================ import { IconType } from '../components/SoundIcon'; interface SoundData { id: string; name: string; category: string; iconType: IconType; audioUrl: string; } // 判断是否在Electron环境中运行 const isElectron = () => { return window.navigator.userAgent.includes('Electron'); }; // 获取正确的音频URL前缀 const getAudioUrlPrefix = () => { return isElectron() ? 'app:/' : ''; }; export const sounds: SoundData[] = [ // Nature Category { id: 'forest-birds', name: '森林鸟鸣', category: '自然', iconType: 'forest', audioUrl: `${getAudioUrlPrefix()}/sounds/forest-birds.mp3` }, { id: 'waves', name: '海浪', category: '自然', iconType: 'waves', audioUrl: `${getAudioUrlPrefix()}/sounds/waves.mp3` }, { id: 'creek', name: '溪流', category: '自然', iconType: 'creek', audioUrl: `${getAudioUrlPrefix()}/sounds/creek.mp3` }, { id: 'wind', name: '微风', category: '自然', iconType: 'wind', audioUrl: `${getAudioUrlPrefix()}/sounds/wind.mp3` }, { id: 'leaves-rustling', name: '树叶沙沙', category: '自然', iconType: 'leaves', audioUrl: `${getAudioUrlPrefix()}/sounds/leaves-rustling.mp3` }, { id: 'waterfall', name: '瀑布', category: '自然', iconType: 'waterfall', audioUrl: `${getAudioUrlPrefix()}/sounds/waterfall.mp3` }, { id: 'bonfire', name: '篝火', category: '自然', iconType: 'fire', audioUrl: `${getAudioUrlPrefix()}/sounds/bonfire.mp3` }, { id: 'beach', name: '海滩', category: '自然', iconType: 'beach', audioUrl: `${getAudioUrlPrefix()}/sounds/beach.mp3` }, { id: 'forest-night', name: '夜晚森林', category: '自然', iconType: 'night-forest', audioUrl: `${getAudioUrlPrefix()}/sounds/forest-night.mp3` }, // Rain Category { id: 'rain-light', name: '小雨', category: '雨声', iconType: 'rain-light', audioUrl: `${getAudioUrlPrefix()}/sounds/rain-light.mp3` }, { id: 'rain-heavy', name: '大雨', category: '雨声', iconType: 'rain-light', audioUrl: `${getAudioUrlPrefix()}/sounds/rain-heavy.mp3` }, { id: 'rain-roof', name: '屋檐雨声', category: '雨声', iconType: 'rain-roof', audioUrl: `${getAudioUrlPrefix()}/sounds/rain-roof.mp3` }, { id: 'rain-window', name: '窗外雨声', category: '雨声', iconType: 'rain-window', audioUrl: `${getAudioUrlPrefix()}/sounds/rain-window.mp3` }, { id: 'rain-thunder', name: '雷雨', category: '雨声', iconType: 'thunder', audioUrl: `${getAudioUrlPrefix()}/sounds/rain-thunder.mp3` }, // { // id: 'rain-umbrella', // name: '雨伞声', // category: '雨声', // iconType: 'rain-umbrella', // audioUrl: `${getAudioUrlPrefix()}/sounds/rain-umbrella.mp3` // }, { id: 'rain-leaves', name: '雨打树叶', category: '雨声', iconType: 'rain-leaves', audioUrl: `${getAudioUrlPrefix()}/sounds/rain-leaves.mp3` }, { id: 'rain-puddle', name: '雨水潭', category: '雨声', iconType: 'rain-puddle', audioUrl: `${getAudioUrlPrefix()}/sounds/rain-puddle.mp3` }, // City Category { id: 'city-traffic', name: '城市交通', category: '城市', iconType: 'traffic', audioUrl: `${getAudioUrlPrefix()}/sounds/city-traffic.mp3` }, { id: 'cafe', name: '咖啡馆', category: '城市', iconType: 'cafe', audioUrl: `${getAudioUrlPrefix()}/sounds/cafe.mp3` }, { id: 'keyboard', name: '键盘声', category: '城市', iconType: 'keyboard', audioUrl: `${getAudioUrlPrefix()}/sounds/keyboard.mp3` }, { id: 'subway', name: '地铁', category: '城市', iconType: 'subway', audioUrl: `${getAudioUrlPrefix()}/sounds/subway.mp3` }, { id: 'park', name: '公园', category: '城市', iconType: 'park', audioUrl: `${getAudioUrlPrefix()}/sounds/park.mp3` }, { id: 'train', name: '火车', category: '城市', iconType: 'train', audioUrl: `${getAudioUrlPrefix()}/sounds/train.mp3` }, // // Animals Category // { // id: 'birds', // name: '鸟鸣', // category: '动物', // iconType: 'birds', // audioUrl: `${getAudioUrlPrefix()}/sounds/birds.mp3` // }, // { // id: 'crickets', // name: '蟋蟀', // category: '动物', // iconType: 'crickets', // audioUrl: `${getAudioUrlPrefix()}/sounds/crickets.mp3` // }, // { // id: 'frogs', // name: '青蛙', // category: '动物', // iconType: 'frogs', // audioUrl: `${getAudioUrlPrefix()}/sounds/frogs.mp3` // }, // { // id: 'seagulls', // name: '海鸥', // category: '动物', // iconType: 'seagulls', // audioUrl: `${getAudioUrlPrefix()}/sounds/seagulls.mp3` // }, // { // id: 'wolves', // name: '狼嚎', // category: '动物', // iconType: 'wolves', // audioUrl: `${getAudioUrlPrefix()}/sounds/wolves.mp3` // }, // { // id: 'owls', // name: '猫头鹰', // category: '动物', // iconType: 'owls', // audioUrl: `${getAudioUrlPrefix()}/sounds/owls.mp3` // }, // { // id: 'cats', // name: '猫咪', // category: '动物', // iconType: 'cats', // audioUrl: `${getAudioUrlPrefix()}/sounds/cats.mp3` // }, // { // id: 'dolphins', // name: '海豚', // category: '动物', // iconType: 'dolphins', // audioUrl: `${getAudioUrlPrefix()}/sounds/dolphins.mp3` // }, // { // id: 'whales', // name: '鲸鱼', // category: '动物', // iconType: 'whales', // audioUrl: `${getAudioUrlPrefix()}/sounds/whales.mp3` // } ]; ================================================ FILE: src/data/themes.ts ================================================ import { Theme } from '../types/theme'; export const themes: Theme[] = [ { id: 'light', name: 'Light', colors: { background: 'bg-gray-50', foreground: 'bg-white', primary: 'bg-purple-600', secondary: 'bg-purple-100', accent: 'bg-purple-500', text: 'text-gray-900', textSecondary: 'text-gray-600', border: 'border-gray-200' } }, { id: 'dark', name: 'Dark', colors: { background: 'bg-gray-900', foreground: 'bg-gray-800', primary: 'bg-purple-500', secondary: 'bg-purple-900', accent: 'bg-purple-400', text: 'text-white', textSecondary: 'text-gray-300', border: 'border-gray-700' } }, { id: 'nature', name: 'Nature', colors: { background: 'bg-green-50', foreground: 'bg-white', primary: 'bg-green-600', secondary: 'bg-green-100', accent: 'bg-green-500', text: 'text-gray-900', textSecondary: 'text-gray-600', border: 'border-green-200' } }, { id: 'ocean', name: 'Ocean', colors: { background: 'bg-blue-50', foreground: 'bg-white', primary: 'bg-blue-600', secondary: 'bg-blue-100', accent: 'bg-blue-500', text: 'text-gray-900', textSecondary: 'text-gray-600', border: 'border-blue-200' } } ]; ================================================ FILE: src/hooks/useOnClickOutside.ts ================================================ import { RefObject, useEffect } from 'react'; export function useOnClickOutside( ref: RefObject, handler: (event: MouseEvent | TouchEvent) => void, ) { useEffect(() => { const listener = (event: MouseEvent | TouchEvent) => { if (!ref.current || ref.current.contains(event.target as Node)) { return; } handler(event); }; document.addEventListener('mousedown', listener); document.addEventListener('touchstart', listener); return () => { document.removeEventListener('mousedown', listener); document.removeEventListener('touchstart', listener); }; }, [ref, handler]); } ================================================ FILE: src/hooks/useSEO.ts ================================================ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useThemeStore } from '../store/useThemeStore'; interface SEOConfig { title?: string; description?: string; keywords?: string; themeColor?: string; } export const useSEO = (config?: SEOConfig) => { const { i18n, t } = useTranslation(); const { getTheme } = useThemeStore(); const theme = getTheme(); useEffect(() => { // 更新页面标题 const title = config?.title || ( i18n.language === 'zh' ? 'Stay Focused - 专注时钟 & 白噪音 | 提升专注力的在线工具' : 'Stay Focused - Focus Timer & White Noise | Boost Your Productivity' ); document.title = title; // 更新描述 const description = config?.description || ( i18n.language === 'zh' ? 'Stay Focused是一款免费的在线专注时钟和白噪音应用,提供多种自然声音如雨声、森林鸟鸣、海浪等,帮助您提升专注力、提高工作效率、改善睡眠质量。支持自定义混音、专注计时器和多种主题。' : 'Stay Focused is a free online focus timer and white noise app with various natural sounds like rain, forest birds, ocean waves to help you boost concentration, improve productivity and enhance sleep quality. Features custom sound mixing, focus timer and multiple themes.' ); const descriptionMeta = document.querySelector('meta[name="description"]'); if (descriptionMeta) { descriptionMeta.setAttribute('content', description); } // 更新语言属性 document.documentElement.lang = i18n.language === 'zh' ? 'zh-CN' : 'en'; // 更新Open Graph标签 const ogTitle = document.querySelector('meta[property="og:title"]'); const ogDescription = document.querySelector('meta[property="og:description"]'); const ogLocale = document.querySelector('meta[property="og:locale"]'); if (ogTitle) { ogTitle.setAttribute('content', i18n.language === 'zh' ? 'Stay Focused - 专注时钟 & 白噪音' : 'Stay Focused - Focus Timer & White Noise'); } if (ogDescription) { ogDescription.setAttribute('content', i18n.language === 'zh' ? '免费的在线专注时钟和白噪音应用,提供多种自然声音,帮助您提升专注力和工作效率。' : 'Free online focus timer and white noise app with various natural sounds to help boost concentration and productivity.'); } if (ogLocale) { ogLocale.setAttribute('content', i18n.language === 'zh' ? 'zh_CN' : 'en_US'); } // 更新Twitter Card标签 const twitterTitle = document.querySelector('meta[name="twitter:title"]'); const twitterDescription = document.querySelector('meta[name="twitter:description"]'); if (twitterTitle) { twitterTitle.setAttribute('content', i18n.language === 'zh' ? 'Stay Focused - 专注时钟 & 白噪音' : 'Stay Focused - Focus Timer & White Noise'); } if (twitterDescription) { twitterDescription.setAttribute('content', i18n.language === 'zh' ? '免费的在线专注时钟和白噪音应用,提供多种自然声音,帮助您提升专注力和工作效率。' : 'Free online focus timer and white noise app with various natural sounds to help boost concentration and productivity.'); } // 根据主题更新主题颜色 const getThemeColor = () => { switch (theme.id) { case 'minimal-light': return '#10b981'; // 绿色 case 'ocean': return '#2563eb'; // 蓝色 case 'warm': return '#f97316'; // 橙色 case 'sunset': return '#dc2626'; // 红色 case 'mint': return '#10b981'; // 薄荷绿 default: return '#10b981'; } }; const themeColor = config?.themeColor || getThemeColor(); const themeColorMeta = document.querySelector('meta[name="theme-color"]'); const msApplicationTileColor = document.querySelector('meta[name="msapplication-TileColor"]'); if (themeColorMeta) { themeColorMeta.setAttribute('content', themeColor); } if (msApplicationTileColor) { msApplicationTileColor.setAttribute('content', themeColor); } // 更新结构化数据 const structuredDataScript = document.querySelector('script[type="application/ld+json"]'); if (structuredDataScript) { try { const structuredData = JSON.parse(structuredDataScript.textContent || '{}'); structuredData.name = i18n.language === 'zh' ? 'Stay Focused' : 'Stay Focused'; structuredData.alternateName = i18n.language === 'zh' ? '专注时钟 & 白噪音' : 'Focus Timer & White Noise'; structuredData.description = i18n.language === 'zh' ? '免费的在线专注时钟和白噪音应用,提供多种自然声音,帮助您提升专注力和工作效率' : 'Free online focus timer and white noise app with various natural sounds to help boost concentration and productivity'; structuredData.inLanguage = i18n.language === 'zh' ? ['zh-CN', 'en-US'] : ['en-US', 'zh-CN']; structuredDataScript.textContent = JSON.stringify(structuredData, null, 2); } catch (error) { console.warn('Failed to update structured data:', error); } } }, [i18n.language, theme.id, config, t]); // 返回当前SEO信息 return { currentLanguage: i18n.language, currentTheme: theme.id, updateSEO: (newConfig: SEOConfig) => { // 可以用于手动更新SEO信息 if (newConfig.title) document.title = newConfig.title; if (newConfig.description) { const meta = document.querySelector('meta[name="description"]'); if (meta) meta.setAttribute('content', newConfig.description); } } }; }; ================================================ FILE: src/hooks/useWebVitals.ts ================================================ /** * Web Vitals 性能监控 Hook * 用于跟踪和报告 Core Web Vitals 指标 */ import { useEffect, useCallback, useRef } from 'react'; export interface WebVitalsMetrics { // Core Web Vitals lcp?: number; // Largest Contentful Paint fid?: number; // First Input Delay (已弃用,使用 INP) inp?: number; // Interaction to Next Paint cls?: number; // Cumulative Layout Shift // 其他重要指标 fcp?: number; // First Contentful Paint ttfb?: number; // Time to First Byte loadTime?: number; // 页面完全加载时间 // 判断值 isGood?: boolean; rating?: 'good' | 'needs-improvement' | 'poor'; } /** * 获取 Web Vitals 评分 */ function getWebVitalsRating(metrics: WebVitalsMetrics): 'good' | 'needs-improvement' | 'poor' { let score = 0; // LCP (最大内容绘制) - 应该 ≤ 2.5s 为 good if (metrics.lcp) { if (metrics.lcp <= 2500) score += 1; else if (metrics.lcp <= 4000) score += 0.5; } // INP (交互到下一次绘制) - 应该 ≤ 200ms 为 good if (metrics.inp) { if (metrics.inp <= 200) score += 1; else if (metrics.inp <= 500) score += 0.5; } // CLS (累计布局移动) - 应该 ≤ 0.1 为 good if (metrics.cls) { if (metrics.cls <= 0.1) score += 1; else if (metrics.cls <= 0.25) score += 0.5; } // FCP (首次内容绘制) - 应该 ≤ 1.8s 为 good if (metrics.fcp) { if (metrics.fcp <= 1800) score += 0.5; else if (metrics.fcp <= 3000) score += 0.25; } const avgScore = score / 4; if (avgScore >= 0.75) return 'good'; if (avgScore >= 0.5) return 'needs-improvement'; return 'poor'; } /** * 监听 Web Vitals 的 Hook */ export const useWebVitals = ( callback?: (metrics: WebVitalsMetrics) => void, debugMode: boolean = false ) => { const metricsRef = useRef({}); const handleMetric = useCallback( (metric: { name: string; value: number; startTime?: number; attribution?: unknown; }) => { const metrics = metricsRef.current; switch (metric.name) { case 'LCP': metrics.lcp = metric.value; break; case 'FID': metrics.fid = metric.value; break; case 'INP': metrics.inp = metric.value; break; case 'CLS': metrics.cls = metric.value; break; case 'FCP': metrics.fcp = metric.value; break; case 'TTFB': metrics.ttfb = metric.value; break; default: break; } metrics.rating = getWebVitalsRating(metrics); metrics.isGood = metrics.rating === 'good'; if (debugMode) { console.log(`[Web Vitals] ${metric.name}: ${metric.value.toFixed(2)}ms`, metrics); } if (callback) { callback(metrics); } }, [callback, debugMode] ); useEffect(() => { // 使用 web-vitals 库(如果可用) try { // 首先尝试使用官方的 web-vitals 库 if ('web-vitals' in window) { // 如果库已加载,使用它的函数 return; } } catch { // web-vitals 库不可用,使用 Performance Observer API } // 备选方案:使用原生 Performance Observer API const performanceObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.entryType === 'largest-contentful-paint') { const lcp = entry.startTime; handleMetric({ name: 'LCP', value: lcp }); } else if (entry.entryType === 'first-input') { const firstInput = entry as unknown as { processingStart?: number; startTime?: number }; handleMetric({ name: 'FID', value: (firstInput.processingStart || 0) - (firstInput.startTime || 0) }); } else if (entry.entryType === 'layout-shift') { const layoutEntry = entry as unknown as { hadRecentInput?: boolean; value?: number }; if (layoutEntry.hadRecentInput) { return; // 忽略用户输入后的布局移动 } const clsValue = layoutEntry.value || 0; metricsRef.current.cls = (metricsRef.current.cls || 0) + clsValue; handleMetric({ name: 'CLS', value: metricsRef.current.cls || 0 }); } else if (entry.entryType === 'paint') { const paintEntry = entry as unknown as { name: string }; if (paintEntry.name === 'first-contentful-paint') { handleMetric({ name: 'FCP', value: entry.startTime }); } } } }); try { performanceObserver.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift', 'paint'] }); } catch (e) { // 某些浏览器可能不支持某些入口类型 console.warn('PerformanceObserver 配置失败:', e); } // 页面卸载时测量 TTFB 和总加载时间 const handleBeforeUnload = () => { if (window.performance && window.performance.timing) { const timing = window.performance.timing; metricsRef.current.ttfb = timing.responseStart - timing.navigationStart; metricsRef.current.loadTime = timing.loadEventEnd - timing.navigationStart; } }; window.addEventListener('beforeunload', handleBeforeUnload); // 在页面加载完成后测量 const handleLoad = () => { setTimeout(() => { if (window.performance && window.performance.timing) { const timing = window.performance.timing; metricsRef.current.ttfb = timing.responseStart - timing.navigationStart; metricsRef.current.loadTime = timing.loadEventEnd - timing.navigationStart; if (debugMode) { console.log('[Web Vitals - Load] TTFB:', metricsRef.current.ttfb, 'Load Time:', metricsRef.current.loadTime); } if (callback) { callback(metricsRef.current); } } }, 0); }; window.addEventListener('load', handleLoad); return () => { performanceObserver.disconnect(); window.removeEventListener('load', handleLoad); window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [handleMetric, callback, debugMode]); return metricsRef.current; }; /** * 格式化 Web Vitals 报告 */ export function formatWebVitalsReport(metrics: WebVitalsMetrics): string { const rating = metrics.rating || 'unknown'; const ratingEmoji = metrics.isGood ? '✅' : metrics.rating === 'needs-improvement' ? '⚠️' : '❌'; return ` ${ratingEmoji} Web Vitals Report - ${rating.toUpperCase()} Core Web Vitals: • LCP (Largest Contentful Paint): ${metrics.lcp ? `${metrics.lcp.toFixed(0)}ms` : 'N/A'} • INP (Interaction to Next Paint): ${metrics.inp ? `${metrics.inp.toFixed(0)}ms` : 'N/A'} • CLS (Cumulative Layout Shift): ${metrics.cls ? metrics.cls.toFixed(3) : 'N/A'} Other Metrics: • FCP (First Contentful Paint): ${metrics.fcp ? `${metrics.fcp.toFixed(0)}ms` : 'N/A'} • TTFB (Time to First Byte): ${metrics.ttfb ? `${metrics.ttfb.toFixed(0)}ms` : 'N/A'} • Load Time: ${metrics.loadTime ? `${(metrics.loadTime / 1000).toFixed(2)}s` : 'N/A'} `; } /** * 生成 Web Vitals HTML 报告 */ export function generateWebVitalsHTMLReport(metrics: WebVitalsMetrics): string { const rating = metrics.rating || 'unknown'; const ratingColor = metrics.rating === 'good' ? '#10b981' : metrics.rating === 'needs-improvement' ? '#f59e0b' : '#ef4444'; return `

📊 Web Vitals Report

LCP
${metrics.lcp ? `${metrics.lcp.toFixed(0)}ms` : 'N/A'}
Largest Contentful Paint
INP
${metrics.inp ? `${metrics.inp.toFixed(0)}ms` : 'N/A'}
Interaction to Next Paint
CLS
${metrics.cls ? metrics.cls.toFixed(3) : 'N/A'}
Cumulative Layout Shift
FCP
${metrics.fcp ? `${metrics.fcp.toFixed(0)}ms` : 'N/A'}
First Contentful Paint
Overall Rating: ${rating.toUpperCase()}
`; } ================================================ FILE: src/i18n.ts ================================================ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import enTranslation from './locales/en/translation.json'; import zhTranslation from './locales/zh/translation.json'; const resources = { en: { translation: enTranslation }, zh: { translation: zhTranslation } }; i18n .use(initReactI18next) .init({ resources, lng: localStorage.getItem('preferred-language') || 'zh', fallbackLng: 'zh', interpolation: { escapeValue: false }, react: { useSuspense: false }, load: 'languageOnly', preload: ['zh', 'en'], initImmediate: false }); export default i18n; ================================================ FILE: src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: src/locales/en/translation.json ================================================ { "app": { "title": "Stay Focused", "description": "Focus Timer & White Noise" }, "categories": { "nature": "Nature", "rain": "Rain", "city": "City", "animals": "Animals" }, "sounds": { "forest-birds": "Forest Birds", "waves": "Ocean Waves", "creek": "Creek", "wind": "Gentle Wind", "leaves-rustling": "Rustling Leaves", "waterfall": "Waterfall", "bonfire": "Bonfire", "beach": "Beach", "forest-night": "Night Forest", "rain-light": "Light Rain", "rain-heavy": "Heavy Rain", "rain-roof": "Rain on Roof", "rain-window": "Rain on Window", "rain-thunder": "Thunderstorm", "rain-leaves": "Rain on Leaves", "rain-puddle": "Rain Puddle", "city-traffic": "City Traffic", "cafe": "Coffee Shop", "keyboard": "Keyboard", "subway": "Subway", "park": "City Park", "train": "Train" }, "timer": { "title": "Focus Time", "start": "Start", "pause": "Pause", "reset": "Reset", "minutes": "min", "completeTitle": "Focus Time Complete", "completeMessage": "Great job! You've completed your focus session. Take a break and get ready for the next one!", "customMinutes": "Custom Time" }, "mixes": { "title": "My Mixes", "save": "Save Current Mix", "saved": "Saved Mixes", "namePlaceholder": "Name your mix", "noSounds": "No sounds", "mix": "Mix" }, "themes": { "title": "Theme Settings", "minimal-light": "Minimal Light", "nature": "Nature Green", "ocean": "Ocean Blue", "warm": "Warm Orange", "sunset": "Sunset", "mint": "Mint", "toggle": "Toggle Theme" } } ================================================ FILE: src/locales/en.json ================================================ { "app": { "title": "StayFocused White Noise", "description": "Focus and relax with ambient sounds" }, "controls": { "play": "Play", "pause": "Pause", "stop": "Stop", "volume": "Volume" } } ================================================ FILE: src/locales/zh/translation.json ================================================ { "app": { "title": "专注时钟", "description": "专注时钟 & 白噪音" }, "categories": { "nature": "自然", "rain": "雨声", "city": "城市", "animals": "动物" }, "sounds": { "forest-birds": "森林鸟鸣", "waves": "海浪", "creek": "溪流", "wind": "微风", "leaves-rustling": "树叶沙沙", "waterfall": "瀑布", "bonfire": "篝火", "beach": "海滩", "forest-night": "夜晚森林", "rain-light": "小雨", "rain-heavy": "大雨", "rain-roof": "屋檐雨声", "rain-window": "窗外雨声", "rain-thunder": "雷雨", "rain-leaves": "雨打树叶", "rain-puddle": "雨水潭", "city-traffic": "城市交通", "cafe": "咖啡馆", "keyboard": "键盘声", "subway": "地铁", "park": "公园", "train": "火车" }, "timer": { "title": "专注时间", "start": "开始", "pause": "暂停", "reset": "重置", "minutes": "分钟", "completeTitle": "专注时间结束", "completeMessage": "太棒了!你已经完成了这次专注。休息一下,准备开始新的专注吧!", "customMinutes": "自定义时间" }, "mixes": { "title": "我的混音", "save": "保存当前混音", "saved": "已保存的混音", "namePlaceholder": "为你的混音命名", "noSounds": "无声音", "mix": "混音" }, "themes": { "title": "主题设置", "minimal-light": "极简白", "nature": "自然绿", "ocean": "海洋蓝", "warm": "温暖橙", "sunset": "日落", "mint": "薄荷", "toggle": "切换主题" } } ================================================ FILE: src/locales/zh.json ================================================ { "app": { "title": "StayFocused 白噪音", "description": "专注和放松的环境音效" }, "controls": { "play": "播放", "pause": "暂停", "stop": "停止", "volume": "音量" } } ================================================ FILE: src/main.tsx ================================================ import React, { Suspense } from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; import './i18n'; import { useThemeStore } from './store/useThemeStore'; const LoadingFallback = () => { const { getTheme } = useThemeStore(); const theme = getTheme(); return (
Loading...
); }; ReactDOM.createRoot(document.getElementById('root')!).render( }> ); ================================================ FILE: src/store/timerStore.ts ================================================ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface TimerState { minutes: number; isRunning: boolean; soundEnabled: boolean; actions: { setMinutes: (minutes: number) => void; start: () => void; stop: () => void; reset: () => void; toggleSound: () => void; }; } export const useTimerStore = create()( persist( (set) => ({ minutes: 25, isRunning: false, soundEnabled: true, actions: { setMinutes: (minutes: number) => { if (minutes >= 1 && minutes <= 999) { set({ minutes, isRunning: false }); } }, start: () => set({ isRunning: true }), stop: () => set({ isRunning: false }), reset: () => set({ isRunning: false }), toggleSound: () => set((state) => ({ soundEnabled: !state.soundEnabled })), }, }), { name: 'timer-storage', partialize: (state) => ({ minutes: state.minutes, soundEnabled: state.soundEnabled, }), } ) ); ================================================ FILE: src/store/useStore.ts ================================================ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { Sound, SoundMix } from '../types'; import { sounds } from '../data/sounds'; interface State { activeSounds: Map; savedMixes: SoundMix[]; timerMinutes: number; isTimerRunning: boolean; addSound: (sound: Sound) => void; removeSound: (soundId: string) => void; setVolume: (soundId: string, volume: number) => void; saveMix: (name: string) => void; loadMix: (mix: SoundMix) => void; deleteMix: (index: number) => void; setTimer: (minutes: number) => void; startTimer: () => void; stopTimer: () => void; } export const useStore = create()( persist( (set, get) => ({ activeSounds: new Map(), savedMixes: [], timerMinutes: 25, isTimerRunning: false, addSound: (sound) => { const { activeSounds } = get(); if (!activeSounds.has(sound.id)) { const audio = new Audio(sound.audioUrl); audio.loop = true; audio.volume = 0.5; audio.play(); set((state) => ({ activeSounds: new Map(state.activeSounds).set(sound.id, { volume: 0.5, audio, }), })); } }, removeSound: (soundId) => { const { activeSounds } = get(); const sound = activeSounds.get(soundId); if (sound) { sound.audio.pause(); sound.audio.currentTime = 0; activeSounds.delete(soundId); set({ activeSounds: new Map(activeSounds) }); } }, setVolume: (soundId, volume) => { const { activeSounds } = get(); const sound = activeSounds.get(soundId); if (sound) { sound.audio.volume = volume; set({ activeSounds: new Map(activeSounds).set(soundId, { ...sound, volume, }), }); } }, saveMix: (name) => { const { activeSounds } = get(); const volumes: Record = {}; activeSounds.forEach((sound, soundId) => { volumes[soundId] = sound.volume; }); set((state) => ({ savedMixes: [ ...state.savedMixes, { id: Date.now().toString(), name, volumes, }, ], })); }, loadMix: (mix) => { const { activeSounds } = get(); // 停止并清除所有当前播放的声音 activeSounds.forEach((sound) => { sound.audio.pause(); sound.audio.currentTime = 0; }); // 加载新的混音 const newActiveSounds = new Map(); Object.entries(mix.volumes).forEach(([soundId, volume]) => { const sound = sounds.find(s => s.id === soundId); if (sound) { const audio = new Audio(sound.audioUrl); audio.loop = true; audio.volume = volume; audio.play(); newActiveSounds.set(soundId, { volume, audio }); } }); set({ activeSounds: newActiveSounds }); }, deleteMix: (index) => { set((state) => ({ savedMixes: state.savedMixes.filter((_, i) => i !== index) })); }, setTimer: (minutes: number) => { if (minutes >= 1 && minutes <= 999) { console.log('Store: Setting timer to:', minutes); // 添加日志 set((state) => ({ ...state, timerMinutes: minutes, isTimerRunning: false // 设置新时间时停止计时器 })); } }, startTimer: () => { set((state) => ({ ...state, isTimerRunning: true })); }, stopTimer: () => { set((state) => ({ ...state, isTimerRunning: false })); }, }), { name: 'stayfocused-storage', partialize: (state) => ({ savedMixes: state.savedMixes, timerMinutes: state.timerMinutes, }), } ) ); ================================================ FILE: src/store/useThemeStore.ts ================================================ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { Theme, themes } from '../themes'; interface ThemeState { currentThemeId: string; getTheme: () => Theme; setTheme: (themeId: string) => void; } export const useThemeStore = create()( persist( (set, get) => ({ currentThemeId: 'warm', getTheme: () => themes.find(theme => theme.id === get().currentThemeId) || themes[0], setTheme: (themeId: string) => { if (themes.some(theme => theme.id === themeId)) { set({ currentThemeId: themeId }); } }, }), { name: 'theme-storage', } ) ); ================================================ FILE: src/themes/index.ts ================================================ export interface Theme { id: string; colors: { background: string; foreground: string; primary: string; secondary: string; accent: string; text: string; textPrimary: string; textSecondary: string; border: string; error: string; success: string; warning: string; muted: string; }; gradients: { primary: string; secondary: string; }; shadows: { sm: string; md: string; lg: string; }; } export const themes: Theme[] = [ { id: 'minimal-light', colors: { background: 'bg-gray-50', foreground: 'bg-white', primary: 'bg-blue-600', secondary: 'bg-gray-100', accent: 'text-blue-600', text: 'text-gray-700', textPrimary: 'text-gray-900', textSecondary: 'text-gray-500', border: 'border-gray-200', error: 'text-red-500', success: 'text-green-500', warning: 'text-yellow-500', muted: 'text-gray-500', }, gradients: { primary: 'bg-gradient-to-r from-blue-500 to-blue-600', secondary: 'bg-gradient-to-r from-gray-100 to-gray-200', }, shadows: { sm: 'shadow-sm', md: 'shadow-md', lg: 'shadow-lg', }, }, { id: 'nature', colors: { background: 'bg-green-50', foreground: 'bg-white', primary: 'bg-green-600', secondary: 'bg-green-100', accent: 'text-green-600', text: 'text-gray-700', textPrimary: 'text-gray-900', textSecondary: 'text-gray-500', border: 'border-green-200', error: 'text-red-500', success: 'text-green-500', warning: 'text-yellow-500', muted: 'text-gray-500', }, gradients: { primary: 'bg-gradient-to-r from-green-500 to-green-600', secondary: 'bg-gradient-to-r from-green-100 to-green-200', }, shadows: { sm: 'shadow-sm', md: 'shadow-md', lg: 'shadow-lg', }, }, { id: 'ocean', colors: { background: 'bg-blue-50', foreground: 'bg-white', primary: 'bg-blue-600', secondary: 'bg-blue-100', accent: 'text-blue-600', text: 'text-gray-700', textPrimary: 'text-gray-900', textSecondary: 'text-gray-500', border: 'border-blue-200', error: 'text-red-500', success: 'text-green-500', warning: 'text-yellow-500', muted: 'text-gray-500', }, gradients: { primary: 'bg-gradient-to-r from-blue-500 to-blue-600', secondary: 'bg-gradient-to-r from-blue-100 to-blue-200', }, shadows: { sm: 'shadow-sm', md: 'shadow-md', lg: 'shadow-lg', }, }, { id: 'warm', colors: { background: 'bg-orange-50', foreground: 'bg-white', primary: 'bg-orange-500', secondary: 'bg-orange-100', accent: 'text-orange-500', text: 'text-gray-700', textPrimary: 'text-gray-900', textSecondary: 'text-gray-500', border: 'border-orange-200', error: 'text-red-500', success: 'text-green-500', warning: 'text-yellow-500', muted: 'text-gray-500', }, gradients: { primary: 'bg-gradient-to-r from-orange-400 to-orange-500', secondary: 'bg-gradient-to-r from-orange-100 to-orange-200', }, shadows: { sm: 'shadow-sm', md: 'shadow-md', lg: 'shadow-lg', }, }, { id: 'sunset', colors: { background: 'bg-orange-50', foreground: 'bg-white', primary: 'bg-orange-500', secondary: 'bg-orange-100', accent: 'text-orange-500', text: 'text-orange-900', textPrimary: 'text-orange-900', textSecondary: 'text-orange-600', border: 'border-orange-200', error: 'text-red-500', success: 'text-green-500', warning: 'text-yellow-500', muted: 'text-gray-500', }, gradients: { primary: 'bg-gradient-to-r from-orange-400 to-orange-500', secondary: 'bg-gradient-to-r from-orange-100 to-orange-200', }, shadows: { sm: 'shadow-sm', md: 'shadow-md', lg: 'shadow-lg', }, }, { id: 'mint', colors: { background: 'bg-emerald-50', foreground: 'bg-white', primary: 'bg-emerald-500', secondary: 'bg-emerald-100', accent: 'text-emerald-500', text: 'text-emerald-900', textPrimary: 'text-emerald-900', textSecondary: 'text-emerald-600', border: 'border-emerald-200', error: 'text-red-500', success: 'text-green-500', warning: 'text-yellow-500', muted: 'text-gray-500', }, gradients: { primary: 'bg-gradient-to-r from-emerald-400 to-emerald-500', secondary: 'bg-gradient-to-r from-emerald-100 to-emerald-200', }, shadows: { sm: 'shadow-sm', md: 'shadow-md', lg: 'shadow-lg', }, }, ]; ================================================ FILE: src/types/index.ts ================================================ export interface Sound { id: string; name: string; category: string; audioUrl: string; } export interface SoundMix { id: string; name: string; volumes: Record; } ================================================ FILE: src/types/theme.ts ================================================ export type ThemeType = 'minimal-light' | 'dark' | 'nature' | 'ocean' | 'warm' | 'violet'; export interface Theme { id: string; name: string; colors: { background: string; foreground: string; primary: string; secondary: string; accent: string; text: string; textPrimary: string; textSecondary: string; border: string; error: string; success: string; warning: string; muted: string; }; gradients: { primary: string; secondary: string; }; shadows: { sm: string; md: string; lg: string; }; } ================================================ FILE: src/types.ts ================================================ import { IconType } from './components/SoundIcon'; export interface Sound { id: string; name: string; category: string; iconType: IconType; audioUrl: string; } export interface ActiveSound extends Sound { volume: number; audio?: HTMLAudioElement; } export interface SoundMix { id: string; name: string; sounds: Array<{ soundId: string; volume: number; }>; } export interface Theme { name: string; colors: { background: string; foreground: string; primary: string; secondary: string; text: string; textSecondary: string; }; } ================================================ FILE: src/utils/analytics.ts ================================================ // Google Analytics 和其他分析工具的集成 declare global { interface Window { gtag: (...args: any[]) => void; dataLayer: any[]; } } // Google Analytics 配置 export const GA_TRACKING_ID = import.meta.env.VITE_GA_TRACKING_ID || ''; // 初始化 Google Analytics export const initGA = () => { if (!GA_TRACKING_ID) { console.warn('Google Analytics tracking ID not found'); return; } // 加载 Google Analytics 脚本 const script1 = document.createElement('script'); script1.async = true; script1.src = `https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`; document.head.appendChild(script1); // 初始化 gtag const script2 = document.createElement('script'); script2.innerHTML = ` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '${GA_TRACKING_ID}', { page_title: document.title, page_location: window.location.href, send_page_view: true }); `; document.head.appendChild(script2); }; // 页面浏览追踪 export const trackPageView = (page_title: string, page_location?: string) => { if (typeof window.gtag !== 'undefined') { window.gtag('config', GA_TRACKING_ID, { page_title, page_location: page_location || window.location.href, }); } }; // 事件追踪 export const trackEvent = (action: string, category: string, label?: string, value?: number) => { if (typeof window.gtag !== 'undefined') { window.gtag('event', action, { event_category: category, event_label: label, value: value, }); } }; // 自定义事件追踪 export const trackCustomEvent = (eventName: string, parameters: Record = {}) => { if (typeof window.gtag !== 'undefined') { window.gtag('event', eventName, parameters); } }; // 用户行为追踪 export const Analytics = { // 音频播放追踪 trackSoundPlay: (soundName: string, category: string) => { trackEvent('play_sound', 'audio', `${category}_${soundName}`); trackCustomEvent('sound_interaction', { sound_name: soundName, sound_category: category, action: 'play' }); }, // 音频停止追踪 trackSoundStop: (soundName: string, category: string, duration?: number) => { trackEvent('stop_sound', 'audio', `${category}_${soundName}`, duration); trackCustomEvent('sound_interaction', { sound_name: soundName, sound_category: category, action: 'stop', duration: duration }); }, // 音量调整追踪 trackVolumeChange: (soundName: string, volume: number) => { trackEvent('volume_change', 'audio', soundName, Math.round(volume * 100)); trackCustomEvent('volume_adjustment', { sound_name: soundName, volume_level: Math.round(volume * 100) }); }, // 计时器使用追踪 trackTimerStart: (duration: number) => { trackEvent('timer_start', 'productivity', 'focus_timer', duration); trackCustomEvent('timer_interaction', { action: 'start', duration_minutes: duration }); }, trackTimerComplete: (duration: number, actualTime: number) => { trackEvent('timer_complete', 'productivity', 'focus_timer', duration); trackCustomEvent('timer_interaction', { action: 'complete', planned_duration: duration, actual_duration: actualTime, completion_rate: Math.round((actualTime / (duration * 60)) * 100) }); }, trackTimerStop: (duration: number, remainingTime: number) => { trackEvent('timer_stop', 'productivity', 'focus_timer', duration); trackCustomEvent('timer_interaction', { action: 'stop', planned_duration: duration, remaining_time: remainingTime, completion_rate: Math.round(((duration * 60 - remainingTime) / (duration * 60)) * 100) }); }, // 主题切换追踪 trackThemeChange: (fromTheme: string, toTheme: string) => { trackEvent('theme_change', 'ui', `${fromTheme}_to_${toTheme}`); trackCustomEvent('theme_interaction', { from_theme: fromTheme, to_theme: toTheme }); }, // 语言切换追踪 trackLanguageChange: (fromLang: string, toLang: string) => { trackEvent('language_change', 'ui', `${fromLang}_to_${toLang}`); trackCustomEvent('language_interaction', { from_language: fromLang, to_language: toLang }); }, // 混音保存追踪 trackMixSave: (mixName: string, soundCount: number) => { trackEvent('mix_save', 'audio', mixName, soundCount); trackCustomEvent('mix_interaction', { action: 'save', mix_name: mixName, sound_count: soundCount }); }, // 混音加载追踪 trackMixLoad: (mixName: string, soundCount: number) => { trackEvent('mix_load', 'audio', mixName, soundCount); trackCustomEvent('mix_interaction', { action: 'load', mix_name: mixName, sound_count: soundCount }); }, // 用户参与度追踪 trackEngagement: (action: string, details?: Record) => { trackCustomEvent('user_engagement', { engagement_action: action, timestamp: new Date().toISOString(), ...details }); }, // 错误追踪 trackError: (error: string, context?: string) => { trackEvent('error', 'system', error); trackCustomEvent('error_occurred', { error_message: error, error_context: context, timestamp: new Date().toISOString(), user_agent: navigator.userAgent, url: window.location.href }); }, // 性能追踪 trackPerformance: (metric: string, value: number, unit: string = 'ms') => { trackCustomEvent('performance_metric', { metric_name: metric, metric_value: value, metric_unit: unit, timestamp: new Date().toISOString() }); } }; // Google Search Console 验证 export const addSearchConsoleVerification = (verificationCode: string) => { if (!verificationCode) return; let verificationMeta = document.querySelector('meta[name="google-site-verification"]'); if (!verificationMeta) { verificationMeta = document.createElement('meta'); verificationMeta.setAttribute('name', 'google-site-verification'); document.head.appendChild(verificationMeta); } verificationMeta.setAttribute('content', verificationCode); }; // 百度统计集成(可选) export const initBaiduAnalytics = (baiduId: string) => { if (!baiduId) return; const script = document.createElement('script'); script.innerHTML = ` var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?${baiduId}"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); `; document.head.appendChild(script); }; // 初始化所有分析工具 export const initAnalytics = () => { // 只在生产环境中初始化 if (import.meta.env.MODE === 'production') { initGA(); // 如果有百度统计ID,也初始化百度统计 const baiduId = import.meta.env.VITE_BAIDU_ANALYTICS_ID; if (baiduId) { initBaiduAnalytics(baiduId); } // 如果有Google Search Console验证码,添加验证 const gscVerification = import.meta.env.VITE_GSC_VERIFICATION; if (gscVerification) { addSearchConsoleVerification(gscVerification); } } }; ================================================ FILE: src/utils/audio.ts ================================================ class AudioManager { private static instance: AudioManager; private audioCache: Map; private constructor() { this.audioCache = new Map(); } static getInstance(): AudioManager { if (!AudioManager.instance) { AudioManager.instance = new AudioManager(); } return AudioManager.instance; } loadSound(url: string): HTMLAudioElement { if (!this.audioCache.has(url)) { const audio = new Audio(url); audio.loop = true; this.audioCache.set(url, audio); } return this.audioCache.get(url)!; } play(url: string, volume: number = 0.5): void { const audio = this.loadSound(url); audio.volume = volume; audio.play().catch(error => { console.error('Error playing audio:', error); }); } stop(url: string): void { const audio = this.audioCache.get(url); if (audio) { audio.pause(); audio.currentTime = 0; } } setVolume(url: string, volume: number): void { const audio = this.audioCache.get(url); if (audio) { audio.volume = volume; } } stopAll(): void { this.audioCache.forEach(audio => { audio.pause(); audio.currentTime = 0; }); } } export const audioManager = AudioManager.getInstance(); ================================================ FILE: src/utils/seoAudit.ts ================================================ /** * SEO 审核和性能监控工具 * 用于自动检查页面 SEO 指标和 Core Web Vitals */ interface SEOAuditResult { timestamp: number; score: number; issues: SEOIssue[]; metrics: PerformanceMetrics; warnings: string[]; recommendations: string[]; } interface SEOIssue { type: 'error' | 'warning' | 'info'; title: string; description: string; severity: number; // 1-5,5最严重 } interface PerformanceMetrics { fcp?: number; // First Contentful Paint lcp?: number; // Largest Contentful Paint cls?: number; // Cumulative Layout Shift fid?: number; // First Input Delay ttfb?: number; // Time to First Byte loadTime?: number; // 总加载时间 } /** * SEO 审核类 */ export class SEOAuditor { private issues: SEOIssue[] = []; private warnings: string[] = []; private recommendations: string[] = []; private metrics: PerformanceMetrics = {}; /** * 执行完整的 SEO 审核 */ public audit(): SEOAuditResult { this.issues = []; this.warnings = []; this.recommendations = []; this.metrics = {}; // 执行各项检查 this.checkMetaTags(); this.checkStructuredData(); this.checkImages(); this.checkHeadings(); this.checkLinks(); this.checkAccessibility(); this.checkPerformance(); // 计算总分 (100分制) const score = this.calculateScore(); return { timestamp: Date.now(), score, issues: this.issues, metrics: this.metrics, warnings: this.warnings, recommendations: this.recommendations }; } /** * 检查 Meta 标签 */ private checkMetaTags(): void { // 检查 title const title = document.title; if (!title || title.length === 0) { this.issues.push({ type: 'error', title: '缺少页面标题', description: '每个页面都需要一个唯一的、有描述意义的 标签', severity: 5 }); } else if (title.length < 30) { this.warnings.push('页面标题过短(建议 30-60 字符)'); } else if (title.length > 60) { this.warnings.push('页面标题过长(建议 30-60 字符)'); } // 检查 description const description = document.querySelector('meta[name="description"]'); if (!description || !description.getAttribute('content')) { this.issues.push({ type: 'error', title: '缺少 Meta Description', description: '页面需要一个清晰的 meta description(120-160 字符)', severity: 5 }); } else { const descLength = description.getAttribute('content')?.length || 0; if (descLength < 120) { this.warnings.push(`Meta Description 过短(${descLength}字符,建议 120-160)`); } else if (descLength > 160) { this.warnings.push(`Meta Description 过长(${descLength}字符,建议 120-160)`); } } // 检查 keywords const keywords = document.querySelector('meta[name="keywords"]'); if (!keywords || !keywords.getAttribute('content')) { this.warnings.push('缺少 Meta Keywords(在某些搜索引擎中仍有作用)'); } // 检查 viewport const viewport = document.querySelector('meta[name="viewport"]'); if (!viewport) { this.issues.push({ type: 'error', title: '缺少 Viewport Meta 标签', description: '必须有 viewport meta 标签以支持移动设备响应式设计', severity: 5 }); } // 检查 canonical 链接 const canonical = document.querySelector('link[rel="canonical"]'); if (!canonical) { this.warnings.push('建议添加 Canonical 链接以避免重复内容问题'); } // 检查 Open Graph 标签 const ogTitle = document.querySelector('meta[property="og:title"]'); const ogDescription = document.querySelector('meta[property="og:description"]'); const ogImage = document.querySelector('meta[property="og:image"]'); if (!ogTitle || !ogDescription) { this.warnings.push('建议补充完整的 Open Graph 标签以改进社交媒体分享'); } if (!ogImage) { this.warnings.push('建议添加 og:image 以支持社交媒体分享预览'); } // 检查 Twitter Card const twitterCard = document.querySelector('meta[name="twitter:card"]'); if (!twitterCard) { this.warnings.push('建议添加 Twitter Card 标签以优化 Twitter 分享'); } // 检查语言属性 const htmlLang = document.documentElement.getAttribute('lang'); if (!htmlLang) { this.issues.push({ type: 'warning', title: '缺少 html lang 属性', description: '<html> 标签应该包含 lang 属性(如 lang="zh-CN")', severity: 3 }); } } /** * 检查结构化数据 */ private checkStructuredData(): void { const ldJsonScripts = document.querySelectorAll('script[type="application/ld+json"]'); if (ldJsonScripts.length === 0) { this.warnings.push('建议添加 JSON-LD 结构化数据以帮助搜索引擎理解内容'); return; } let hasWebApplicationSchema = false; let hasFAQSchema = false; ldJsonScripts.forEach((script) => { try { const data = JSON.parse(script.textContent || '{}'); const type = data['@type']; if (type === 'WebApplication' || type === 'SoftwareApplication') { hasWebApplicationSchema = true; } if (type === 'FAQPage') { hasFAQSchema = true; } } catch { this.issues.push({ type: 'error', title: '结构化数据格式错误', description: '某个 JSON-LD 脚本包含无效的 JSON', severity: 3 }); } }); if (!hasWebApplicationSchema) { this.warnings.push('建议添加 WebApplication Schema 来描述应用信息'); } if (!hasFAQSchema) { this.recommendations.push('如果页面有常见问题,添加 FAQPage Schema 能改进搜索结果'); } } /** * 检查图片优化 */ private checkImages(): void { const images = document.querySelectorAll('img'); if (images.length === 0) { return; } let missingAltCount = 0; images.forEach((img) => { const alt = img.getAttribute('alt'); if (!alt || alt.trim().length === 0) { missingAltCount++; } }); if (missingAltCount > 0) { this.issues.push({ type: 'warning', title: `${missingAltCount} 个图片缺少 alt 文本`, description: '所有图片都应该有描述性的 alt 文本来改进无障碍性和 SEO', severity: 3 }); } } /** * 检查标题结构 */ private checkHeadings(): void { const h1s = document.querySelectorAll('h1'); if (h1s.length === 0) { this.warnings.push('页面应该至少包含一个 H1 标题'); } else if (h1s.length > 1) { this.warnings.push(`页面有 ${h1s.length} 个 H1,建议只有 1 个`); } // 检查标题层级是否连贯 const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); if (headings.length > 0) { let prevLevel = 0; headings.forEach((heading) => { const level = parseInt(heading.tagName[1]); if (prevLevel > 0 && level > prevLevel + 1) { this.warnings.push(`标题层级不连贯:从 H${prevLevel} 跳到 H${level}`); } prevLevel = level; }); } } /** * 检查链接 */ private checkLinks(): void { const links = document.querySelectorAll('a'); let missingLinkTextCount = 0; links.forEach((link) => { const href = link.getAttribute('href'); const title = link.getAttribute('title'); const ariaLabel = link.getAttribute('aria-label'); // 检查链接文本或标题 if ( (!link.textContent || link.textContent.trim().length === 0) && !title && !ariaLabel ) { missingLinkTextCount++; } // 检查坏链接(简单检查) if (href && href.startsWith('http') && !href.includes('shutong.work')) { // 外部链接应该有 rel="noopener noreferrer" const rel = link.getAttribute('rel'); if (!rel || (!rel.includes('noopener') && !rel.includes('noreferrer'))) { this.warnings.push('外部链接缺少 rel="noopener noreferrer" 属性'); } } }); if (missingLinkTextCount > 0) { this.warnings.push(`${missingLinkTextCount} 个链接缺少文本或标题描述`); } } /** * 检查无障碍性 */ private checkAccessibility(): void { // 检查 ARIA labels const interactiveElements = document.querySelectorAll( 'button, [role="button"], input, select, textarea' ); let missingLabels = 0; interactiveElements.forEach((el) => { const ariaLabel = el.getAttribute('aria-label'); const ariaLabelledBy = el.getAttribute('aria-labelledby'); const label = el.closest('label'); if (!ariaLabel && !ariaLabelledBy && !label) { missingLabels++; } }); if (missingLabels > 0) { this.recommendations.push(`${missingLabels} 个交互元素缺少标签,可以改进无障碍性`); } // 检查颜色对比度(简单检查) const textElements = document.querySelectorAll('p, span, a, h1, h2, h3, h4, h5, h6'); // 这是一个简化的检查,完整的颜色对比度检查需要更复杂的算法 if (textElements.length > 0) { this.recommendations.push('建议使用 WCAG 颜色对比度检查工具验证文本可读性'); } } /** * 检查性能指标 */ private checkPerformance(): void { // 尝试获取 Web Vitals try { // FCP - First Contentful Paint if ('PerformanceObserver' in window) { const perfObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name === 'first-contentful-paint') { this.metrics.fcp = entry.startTime; } } }); perfObserver.observe({ entryTypes: ['paint', 'largest-contentful-paint'] }); } // 总加载时间 if (window.performance && window.performance.timing) { const pageStartTimeValue = ((window as unknown) as Record<string, unknown>).pageStartTime as number || 0; const navigationStart = window.performance.timing.navigationStart; const loadEventEnd = window.performance.timing.loadEventEnd; if (loadEventEnd > 0) { this.metrics.loadTime = loadEventEnd - navigationStart; } else { // 页面仍在加载 this.metrics.loadTime = performance.now() - (pageStartTimeValue || performance.timing.navigationStart); } } } catch { // 浏览器不支持 Performance API } // 性能建议 if (this.metrics.loadTime && this.metrics.loadTime > 3000) { this.recommendations.push( `页面加载时间较长(${(this.metrics.loadTime / 1000).toFixed(2)}s),考虑优化资源加载` ); } // 检查未压缩的资源 this.checkResourceCompression(); } /** * 检查资源压缩 */ private checkResourceCompression(): void { const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; let largeResourceCount = 0; resources.forEach((resource) => { // 检查是否有压缩标志(通过 Content-Encoding) // 注意:由于跟域限制,我们无法直接获取 response headers,这只是一个建议 // 检查大资源 if (resource.transferSize && resource.transferSize > 1024 * 500) { largeResourceCount++; } }); if (largeResourceCount > 0) { this.recommendations.push(`${largeResourceCount} 个资源文件较大(>500KB),考虑优化或分割`); } } /** * 计算 SEO 评分 */ private calculateScore(): number { let score = 100; // 减分规则 this.issues.forEach((issue) => { score -= issue.severity * 5; // 每个错误减去 5-25 分 }); this.warnings.forEach(() => { score -= 3; // 每个警告减去 3 分 }); return Math.max(0, score); // 最低分为 0 } /** * 生成审核报告(HTML 格式) */ public generateReport(result: SEOAuditResult): string { const issuesHtml = result.issues .map( (issue) => `<div style="margin-bottom: 12px; padding: 12px; border-left: 4px solid ${ issue.type === 'error' ? '#ef4444' : '#f97316' }; background-color: ${ issue.type === 'error' ? '#fee2e2' : '#fef3c7' };"> <strong>${issue.title}</strong> <p style="margin: 8px 0 0 0; font-size: 14px;">${issue.description}</p> </div>` ) .join(''); const warningsHtml = result.warnings.length > 0 ? `<div style="padding: 12px; background-color: #fef3c7; border-radius: 4px; margin-bottom: 16px;"> <strong>⚠️ 警告:</strong> <ul style="margin: 8px 0 0 0;">${result.warnings.map((w) => `<li>${w}</li>`).join('')}</ul> </div>` : ''; const recommendationsHtml = result.recommendations.length > 0 ? `<div style="padding: 12px; background-color: #dbeafe; border-radius: 4px;"> <strong>💡 建议:</strong> <ul style="margin: 8px 0 0 0;">${result.recommendations .map((r) => `<li>${r}</li>`) .join('')}</ul> </div>` : ''; const metricsHtml = `<div style="padding: 12px; background-color: #f0fdf4; border-radius: 4px; margin: 16px 0;"> <strong>📊 性能指标:</strong> <ul style="margin: 8px 0 0 0;"> ${result.metrics.fcp ? `<li>FCP: ${result.metrics.fcp.toFixed(2)}ms</li>` : ''} ${result.metrics.lcp ? `<li>LCP: ${result.metrics.lcp.toFixed(2)}ms</li>` : ''} ${result.metrics.cls ? `<li>CLS: ${result.metrics.cls.toFixed(3)}</li>` : ''} ${ result.metrics.loadTime ? `<li>加载时间: ${(result.metrics.loadTime / 1000).toFixed(2)}s</li>` : '' } </ul> </div>`; return ` <div style="font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 0 auto;"> <h1 style="color: #1f2937; border-bottom: 2px solid #3b82f6; padding-bottom: 12px;"> SEO 审核报告 <span style="float: right; font-size: 32px; color: ${result.score >= 80 ? '#10b981' : result.score >= 60 ? '#f59e0b' : '#ef4444'};"> ${result.score} </span> </h1> <div style="margin: 16px 0;"> <p style="color: #6b7280;">生成时间:${new Date(result.timestamp).toLocaleString()}</p> </div> ${ result.issues.length > 0 ? `<div style="margin: 16px 0;"> <h2 style="color: #ef4444;">❌ 关键问题 (${result.issues.length})</h2> ${issuesHtml} </div>` : '' } ${warningsHtml} ${recommendationsHtml} ${metricsHtml} </div> `; } } /** * 导出单例 */ export const seoAuditor = new SEOAuditor(); /** * 自动执行审核(在页面加载完成后) */ export function initSEOAudit(): void { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { // 延迟执行以确保所有资源都加载 setTimeout(() => { const result = seoAuditor.audit(); logAuditResult(result); }, 1000); }); } else { setTimeout(() => { const result = seoAuditor.audit(); logAuditResult(result); }, 1000); } } /** * 记录审核结果到控制台 */ function logAuditResult(result: SEOAuditResult): void { const style = ` color: white; background: ${result.score >= 80 ? '#10b981' : result.score >= 60 ? '#f59e0b' : '#ef4444'}; padding: 12px 16px; border-radius: 4px; font-size: 16px; font-weight: bold; `; console.group('%c🔍 SEO Audit Report', style); console.log('Score:', result.score, '/ 100'); console.log('Issues:', result.issues.length); console.log('Warnings:', result.warnings.length); console.log('Recommendations:', result.recommendations.length); console.log('Full Result:', result); console.groupEnd(); } ================================================ FILE: src/vite-env.d.ts ================================================ /// <reference types="vite/client" /> ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { extend: {}, }, plugins: [], }; ================================================ FILE: tsconfig.app.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] } ================================================ FILE: tsconfig.json ================================================ { "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] } ================================================ FILE: tsconfig.node.json ================================================ { "compilerOptions": { "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["vite.config.ts"] } ================================================ FILE: vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], base: './', build: { outDir: 'dist', assetsDir: 'assets', emptyOutDir: true, // 代码分割优化 rollupOptions: { output: { manualChunks: { // 将React相关库分离到单独的chunk 'react-vendor': ['react', 'react-dom'], // 将UI组件库分离 'ui-vendor': ['lucide-react'], // 将国际化相关分离 'i18n-vendor': ['i18next', 'react-i18next'], // 将状态管理分离 'store-vendor': ['zustand'] } } }, // 启用gzip压缩 minify: 'terser', terserOptions: { compress: { drop_console: true, // 生产环境移除console drop_debugger: true } }, // 资源内联阈值 assetsInlineLimit: 4096, // 启用CSS代码分割 cssCodeSplit: true }, // 开发服务器配置 server: { port: 5173, https: false, // 预热常用文件 warmup: { clientFiles: ['./src/main.tsx', './src/App.tsx'] } }, // 预构建优化 optimizeDeps: { include: ['react', 'react-dom', 'zustand', 'i18next', 'react-i18next'], exclude: ['lucide-react'] } });