二十四节气换肤打包(React + Webpack)

43 阅读19分钟

本指南将项目的所有文件与源码完整收录。任何环境下,按照步骤创建目录与文件、粘贴代码、安装依赖、执行命令,即可 100% 复现。

运行环境

  • Node.js: 建议 >= 16
  • 包管理器: npm 或 yarn(以下以 npm 为例)
  • 操作系统: macOS / Windows / Linux
  • 推荐编辑器: VS Code(安装 ESLint、Prettier 插件更佳)

快速开始

  1. 在任意位置新建项目根目录,如 face-webpack-react
  2. 按下文目录结构创建文件夹与文件,将对应代码粘贴进去
  3. 进入项目根目录执行:
npm install
  1. 单入口开发(SPA):
npm run dev

访问 http://localhost:3000/

  1. 多入口(每个节气单独页面)开发(举例“立春”):
npm run start lichun

访问 http://localhost:3000/lichun/

  1. 构建全部节气:
npm run build all

产物在 dist/<season>/


目录结构(请按此创建所有目录与文件)

  • package.json
  • tsconfig.json
  • .babelrc
  • .eslintrc.js
  • .gitignore
  • webpack.config.js
  • webpack.multi.config.js
  • scripts/
    • season-command.js
  • public/
    • index.html
    • season.html
  • src/
    • index.tsx
    • App.tsx
    • seasonEntry.tsx
    • styles/
      • global.css
    • hooks/
      • useTheme.ts
    • data/
      • seasons.ts
    • components/
      • ThemeProvider.tsx
      • SeasonPage.tsx
      • SeasonSelector.tsx
      • ColorPalette.tsx
      • ActionButtons.tsx
      • SeasonCard.tsx
      • Toast.tsx
    • types/
      • global.d.ts
    • pages/ ← 预留(可为空)
    • utils/ ← 预留(可为空)

根目录文件

package.json

{
  "name": "face-webpack-react",
  "version": "2.0.0",
  "description": "二十四节气主题页面 - React重构版",
  "main": "index.js",
  "scripts": {
    "dev": "webpack serve --mode development",
    "build": "node scripts/season-command.js build",
    "build:analyze": "ANALYZE=true webpack --mode production",
    "start": "node scripts/season-command.js start",
    "preview": "http-server dist -p 8080",
    "clean": "rimraf dist",
    "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --max-warnings=0",
    "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
    "format": "prettier --write \"**/*.{js,jsx,ts,tsx,css,html,json,md}\"",
    "type-check": "tsc --noEmit",
    "dev:multi": "webpack serve --config webpack.multi.config.js --mode development",
    "build:multi": "webpack --config webpack.multi.config.js --mode production",
    "build:multi:all": "webpack --config webpack.multi.config.js --mode production",
    "build:multi:analyze": "ANALYZE=true webpack --config webpack.multi.config.js --mode production"
  },
  "keywords": [
    "react",
    "webpack",
    "二十四节气",
    "主题页面",
    "单页应用",
    "传统文化"
  ],
  "author": "Face Webpack Team",
  "license": "MIT",
  "dependencies": {
    "core-js": "^3.45.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.8.0"
  },
  "devDependencies": {
    "@babel/core": "^7.23.0",
    "@babel/preset-env": "^7.23.0",
    "@babel/preset-react": "^7.22.0",
    "@babel/preset-typescript": "^7.23.0",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.6.1",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "babel-loader": "^9.1.3",
    "css-loader": "^6.8.1",
    "eslint": "^8.50.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.1",
    "eslint-plugin-react": "^7.33.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "html-webpack-plugin": "^5.5.3",
    "http-server": "^14.1.1",
    "prettier": "^3.0.3",
    "react-refresh": "^0.17.0",
    "rimraf": "^5.0.5",
    "style-loader": "^3.3.3",
    "typescript": "^5.0.0",
    "webpack": "^5.88.2",
    "webpack-bundle-analyzer": "^4.9.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  },
  "engines": {
    "node": ">=16.0.0"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not op_mini all"
  ]
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES6"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "src",
    "paths": {
      "@/*": ["*"],
      "@/components/*": ["components/*"],
      "@/pages/*": ["pages/*"],
      "@/hooks/*": ["hooks/*"],
      "@/utils/*": ["utils/*"],
      "@/styles/*": ["styles/*"]
    }
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "browsers": [">0.2%", "not dead", "not op_mini all"]
        },
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    [
      "@babel/preset-react",
      {
        "runtime": "automatic"
      }
    ],
    "@babel/preset-typescript"
  ],
  "plugins": [],
  "env": {}
}

.eslintrc.js

module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  plugins: [
    'react',
    'react-hooks',
    '@typescript-eslint',
    'prettier'
  ],
  rules: {
    'prettier/prettier': 'error',
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': 'warn',
    'no-console': 'warn',
    'no-debugger': 'error'
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
};

.gitignore

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
**/node_modules
# roadhog-api-doc ignore
/src/utils/request-temp.js
_roadhog-api-doc

# production
/dist
/.vscode

# misc
.DS_Store
npm-debug.log*
yarn-error.log

/coverage
.idea
yarn.lock
package-lock.json
*bak
.vscode

lib

webpack.config.js(单入口 SPA)

/**
 * Webpack 配置 - React重构版
 * 单入口应用,支持代码分割和优化
 */

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = (env = {}, argv = {}) => {
  const isDev = argv.mode === 'development';
  const isAnalyze = Boolean(env.ANALYZE || process.env.ANALYZE);
  
  return {
    // 构建模式
    mode: isDev ? 'development' : 'production',
    
    // 入口配置 - 单入口
    entry: './src/index.tsx',
    
    // 输出配置
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isDev ? '[name].js' : '[name].[contenthash:8].js',
      chunkFilename: isDev ? '[name].chunk.js' : '[name].[contenthash:8].chunk.js',
      assetModuleFilename: 'assets/[name].[contenthash:8][ext][query]',
      publicPath: '/',
      clean: true
    },
    
    // Source Map配置:生产使用 hidden-source-map,保留定位同时降低源码暴露
    devtool: isDev ? 'cheap-module-source-map' : 'hidden-source-map',

    // 启用持久化缓存:大幅缩短二次构建时间
    cache: { type: 'filesystem' },
    
    // 性能配置
    performance: { hints: false },
    
    // 模块处理规则
    module: {
      rules: [
        {
          // 处理TypeScript和JavaScript文件
          test: /\.(ts|tsx|js|jsx)$/,
          exclude: /node_modules/,
          // 使用 .babelrc 基础配置,并在开发环境附加 react-refresh 插件,避免生产注入
          use: {
            loader: 'babel-loader',
            options: {
              plugins: [
                isDev && require.resolve('react-refresh/babel')
              ].filter(Boolean)
            }
          }
        },
        
        // CSS文件处理
        {
          test: /\.css$/,
          use: [
            'style-loader',
            {
              loader: 'css-loader',
              options: {
                modules: false,
                importLoaders: 1
              }
            }
          ]
        },
        
        // 图片资源处理
        {
          test: /\.(png|jpe?g|gif|svg|webp)$/i,
          type: 'asset',
          parser: {
            dataUrlCondition: {
              maxSize: 8 * 1024 // 8KB阈值
            }
          }
        },
        
        // 字体文件处理
        {
          test: /\.(woff2?|ttf|eot|otf)$/i,
          type: 'asset/resource'
        },
        
        // 音视频文件处理
        {
          test: /\.(mp4|mp3|wav|ogg)$/i,
          type: 'asset/resource'
        }
      ]
    },
    
    // 解析配置
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
      alias: {
        '@': path.resolve(__dirname, 'src'),
        '@/components': path.resolve(__dirname, 'src/components'),
        '@/pages': path.resolve(__dirname, 'src/pages'),
        '@/hooks': path.resolve(__dirname, 'src/hooks'),
        '@/utils': path.resolve(__dirname, 'src/utils'),
        '@/styles': path.resolve(__dirname, 'src/styles'),
        '@/data': path.resolve(__dirname, 'src/data')
      }
    },
    
    // 插件配置
    plugins: [
      // HTML模板处理插件
      new HtmlWebpackPlugin({
        template: 'public/index.html',
        filename: 'index.html',
        inject: true,
        minify: isDev ? false : {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true
        }
      }),
      
      // 定义环境变量
      new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production')
      }),
      
      // 构建进度显示插件
      new webpack.ProgressPlugin(),
      
      // 开发环境专用插件
      ...(isDev ? [
        new ReactRefreshWebpackPlugin()
      ] : []),

      // 按需产物分析(以静态报告输出到 dist/report.html)
      ...(isAnalyze ? [
        new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)({
          analyzerMode: 'static',
          openAnalyzer: false,
          reportFilename: path.resolve(__dirname, 'dist/report.html')
        })
      ] : [])
    ],
    
    // 优化配置
    optimization: {
      // 代码分割配置
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          // React相关库单独打包
          react: {
            test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
            name: 'react-vendor',
            chunks: 'all',
            priority: 30
          },
          
          // 其他第三方库统一打包
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
            priority: 20
          },
          
          // 公共代码单独打包
          common: {
            name: 'common',
            minChunks: 2,
            chunks: 'all',
            priority: 10,
            reuseExistingChunk: true
          }
        }
      },
      
      // 运行时代码单独打包
      runtimeChunk: 'single',
      
      // 生产环境优化
      ...(isDev ? {} : {
        minimize: true,
        usedExports: true
      })
    },
    
    // 开发服务器配置
    devServer: {
      static: {
        directory: path.resolve(__dirname, 'public')
      },
      compress: true,
      port: 3000,
      open: true,
      hot: true,
      historyApiFallback: true,
      client: {
        overlay: {
          errors: true,
          warnings: false
        }
      }
    },
    
    // 构建日志配置
    stats: isDev ? 'minimal' : 'normal'
  };
};

webpack.multi.config.js(多入口:按节气分别构建)

/**
 * Webpack 多子项目构建配置
 * 根据节气配置文件生成独立的子项目
 */

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

// 读取节气配置
const fs = require('fs');

// 读取节气配置文件
const seasonsPath = path.resolve(__dirname, 'src/data/seasons.ts');
const seasonsContent = fs.readFileSync(seasonsPath, 'utf8');

// 使用正则提取导出的 SEASONS 数组字面量内容(不依赖 TypeScript 运行时)
const seasonsMatch = seasonsContent.match(/export const SEASONS[^=]*=\s*\[([\s\S]*?)\];/);
if (!seasonsMatch) {
  throw new Error('无法解析节气配置文件: 未找到 SEASONS 导出');
}

// 基于源文件内容动态求值,确保数据单一来源
// 说明:
// - 仅对受控的本地源码求值,不涉及外部输入,安全可控
// - 避免硬编码 SEASONS,降低维护成本,避免数据漂移
let SEASONS = [];
try {
  const seasonsArrayCode = seasonsMatch[1];
  // 使用 Function 包裹为数组并求值(支持单引号、尾逗号等 JS 语法)
  SEASONS = new Function(`return [${seasonsArrayCode}]`)();
} catch (e) {
  throw new Error(`解析节气配置失败: ${e.message}`);
}

/**
 * 生成所有节气的webpack配置
 * 每个节气都有独立的入口和输出
 */
const seasonApps = SEASONS.map(season => ({
  name: season.id,
  title: `${season.name} - 二十四节气主题页面`,
  entry: './src/seasonEntry.tsx',
  description: season.description,
  date: season.date,
  seasonData: season
}));

/**
 * 生成单个节气的Webpack配置
 * @param {Object} appConfig - 应用配置对象
 * @param {boolean} isDev - 是否为开发模式
 * @returns {Object} Webpack配置对象
 */
const makeSeasonConfig = ({ name, title, entry, description, date, seasonData }, isDev, isAnalyze) => ({
  // 配置名称
  name,
  
  // 构建模式
  mode: isDev ? 'development' : 'production',
  
  // 入口配置
  entry: { [name]: entry },
  
  // 输出配置
  output: {
    // 输出目录:dist/节气名称/
    path: path.resolve(__dirname, 'dist', name),
    
    // 输出文件名模板
    filename: isDev ? '[name].js' : '[name].[contenthash:8].js',
    chunkFilename: isDev ? '[name].chunk.js' : '[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext][query]',
    
    // 公共资源路径
    publicPath: `/${name}/`,
    
    // 构建前清理输出目录
    clean: true
  },
  
  // Source Map配置
  // 开发保留快速增量映射,生产使用 hidden-source-map 便于定位同时减少源码暴露
  devtool: isDev ? 'cheap-module-source-map' : 'hidden-source-map',
  
  // 构建缓存:显著提升二次构建和多项目并行构建速度
  cache: { type: 'filesystem' },

  // 性能配置
  performance: { hints: false },
  
  // 模块处理规则
  module: {
    rules: [
      {
        // 处理TypeScript和JavaScript文件
        test: /\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            // 使用 .babelrc 的基础配置,并在开发环境附加 react-refresh,避免生产注入
            plugins: [
              isDev && require.resolve('react-refresh/babel')
            ].filter(Boolean)
          }
        }
      },
      
      // CSS文件处理
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: false,
              importLoaders: 1
            }
          }
        ]
      },
      
      // 图片资源处理
      {
        test: /\.(png|jpe?g|gif|svg|webp)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024 // 8KB阈值
          }
        }
      },
      
      // 字体文件处理
      {
        test: /\.(woff2?|ttf|eot|otf)$/i,
        type: 'asset/resource'
      },
      
      // 音视频文件处理
      {
        test: /\.(mp4|mp3|wav|ogg)$/i,
        type: 'asset/resource'
      }
    ]
  },
  
  // 解析配置
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@/components': path.resolve(__dirname, 'src/components'),
      '@/pages': path.resolve(__dirname, 'src/pages'),
      '@/hooks': path.resolve(__dirname, 'src/hooks'),
      '@/utils': path.resolve(__dirname, 'src/utils'),
      '@/styles': path.resolve(__dirname, 'src/styles'),
      '@/data': path.resolve(__dirname, 'src/data')
    }
  },
  
  // 插件配置
  plugins: [
    // HTML模板处理插件
    new HtmlWebpackPlugin({
      template: 'public/season.html',
      filename: 'index.html',
      inject: true,
      title,
      description,
      date,
      seasonId: name,
      seasonData: seasonData,
      chunks: [name],
      minify: isDev ? false : {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      }
    }),
    
    // 定义环境变量(仅保留 NODE_ENV,避免重复的注入渠道)
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production')
    }),
    
    // 构建进度显示插件
    new webpack.ProgressPlugin(),
    
    // 开发环境专用插件
    ...(isDev ? [
      new ReactRefreshWebpackPlugin()
    ] : []),

    // 按需分析构建产物体积
    ...(isAnalyze ? [
      // 延迟加载以避免非 Analyze 构建的额外开销
      new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)({
        analyzerMode: 'static',
        openAnalyzer: false,
        reportFilename: path.resolve(__dirname, `dist/${name}/report.html`)
      })
    ] : [])
  ],
  
  // 优化配置
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // React相关库单独打包
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
          name: 'react-vendor',
          chunks: 'all',
          priority: 30
        },
        
        // 其他第三方库统一打包
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 20
        },
        
        // 公共代码单独打包
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 10,
          reuseExistingChunk: true
        }
      }
    },
    
    // 运行时代码单独打包
    runtimeChunk: 'single',
    
    // 生产环境优化
    ...(isDev ? {} : {
      minimize: true,
      // 让 webpack 按默认策略使用 package.json sideEffects,避免误伤三方包
      usedExports: true
    })
  },
  
  // 开发服务器配置
  devServer: {
    static: {
      directory: path.resolve(__dirname, 'public')
    },
    compress: true,
    port: 3000,
    open: true,
    hot: true,
    // 在 TARGET 模式下,自动将根路径重写到对应子项目的 publicPath,便于直接打开正确页面
    historyApiFallback: {
      rewrites: [
        { from: /^\/$/, to: `/${name}/` }
      ]
    },
    client: {
      overlay: {
        errors: true,
        warnings: false
      }
    }
  },
  
  // 构建日志配置
  stats: isDev ? 'minimal' : 'normal'
});

/**
 * Webpack配置导出函数
 * 
 * 支持两种使用方式:
 * 1. 同时构建所有节气:webpack --config webpack.multi.config.js
 * 2. 单独构建某个节气:webpack --config webpack.multi.config.js --env TARGET=lichun
 * 
 * @param {Object} env - 环境变量对象
 * @param {string} env.TARGET - 指定构建的节气名称
 * @param {Object} argv - webpack命令行参数
 * @param {string} argv.mode - 构建模式(development/production)
 */
module.exports = (env = {}, argv = {}) => {
  // 判断是否为开发模式
  const isDev = argv.mode === 'development';
  // 是否开启产物分析
  const isAnalyze = Boolean(env.ANALYZE || process.env.ANALYZE);
  
  // 如果指定了TARGET,只构建单个节气
  if (env.TARGET) {
    const meta = seasonApps.find(a => a.name === env.TARGET);
    if (!meta) {
      // 友好的错误提示,列出所有可用节气
      throw new Error(
        `Unknown TARGET "${env.TARGET}". Use one of: ${seasonApps.map(a => a.name).join(', ')}`
      );
    }
    return makeSeasonConfig(meta, isDev, isAnalyze);
  }
  
  // 默认构建所有节气
  // 返回数组配置,webpack会并行构建所有节气
  return seasonApps.map(meta => makeSeasonConfig(meta, isDev, isAnalyze));
};

scripts 目录

scripts/season-command.js

#!/usr/bin/env node

/**
 * 节气命令处理脚本
 * 支持简化的命令格式:npm run build [节气名] 和 npm run start [节气名]
 */

const { execSync } = require('child_process');
const path = require('path');

// 24节气配置
const SEASONS = [
  'lichun', 'yushui', 'jingzhe', 'chunfen', 'qingming', 'guyu',
  'lixia', 'xiaoman', 'mangzhong', 'xiazhi', 'xiaoshu', 'dashu',
  'liqiu', 'chushu', 'bailu', 'qiufen', 'hanlu', 'shuangjiang',
  'lidong', 'xiaoxue', 'daxue', 'dongzhi', 'xiaohan', 'dahan'
];

// 节气中文名称映射
const SEASON_NAMES = {
  'lichun': '立春', 'yushui': '雨水', 'jingzhe': '惊蛰', 'chunfen': '春分',
  'qingming': '清明', 'guyu': '谷雨', 'lixia': '立夏', 'xiaoman': '小满',
  'mangzhong': '芒种', 'xiazhi': '夏至', 'xiaoshu': '小暑', 'dashu': '大暑',
  'liqiu': '立秋', 'chushu': '处暑', 'bailu': '白露', 'qiufen': '秋分',
  'hanlu': '寒露', 'shuangjiang': '霜降', 'lidong': '立冬', 'xiaoxue': '小雪',
  'daxue': '大雪', 'dongzhi': '冬至', 'xiaohan': '小寒', 'dahan': '大寒'
};

/**
 * 显示帮助信息
 */
function showHelp() {
  console.log('\n🌿 二十四节气构建工具\n');
  console.log('用法:');
  console.log('  npm run build [节气名]    构建指定节气');
  console.log('  npm run start [节气名]    启动指定节气开发服务器');
  console.log('  npm run build all         构建所有24个节气');
  console.log('  npm run start all         启动所有节气开发服务器\n');
  
  console.log('可用节气:');
  SEASONS.forEach((season, index) => {
    const name = SEASON_NAMES[season];
    const padding = ' '.repeat(12 - season.length);
    console.log(`  ${season}${padding}${name}`);
  });
  console.log('\n示例:');
  console.log('  npm run build lichun      构建立春主题');
  console.log('  npm run start yushui      启动雨水开发服务器');
  console.log('  npm run build all         构建所有节气\n');
}

/**
 * 验证节气名称
 * @param {string} seasonName - 节气名称
 * @returns {string|null} - 返回有效的节气名称或null
 */
function validateSeason(seasonName) {
  if (!seasonName) return null;
  
  const lowerSeason = seasonName.toLowerCase();
  
  // 直接匹配
  if (SEASONS.includes(lowerSeason)) {
    return lowerSeason;
  }
  
  // 中文名称匹配
  for (const [key, value] of Object.entries(SEASON_NAMES)) {
    if (value === seasonName) {
      return key;
    }
  }
  
  // 模糊匹配
  const fuzzyMatch = SEASONS.find(season => 
    season.includes(lowerSeason) || lowerSeason.includes(season)
  );
  
  return fuzzyMatch || null;
}

/**
 * 执行构建命令
 * @param {string} seasonName - 节气名称
 */
function buildSeason(seasonName) {
  if (seasonName === 'all') {
    console.log('🚀 开始构建所有24个节气...\n');
    try {
      execSync('webpack --config webpack.multi.config.js --mode production', {
        stdio: 'inherit',
        cwd: process.cwd()
      });
      console.log('\n✅ 所有节气构建完成!');
    } catch (error) {
      console.error('\n❌ 构建失败:', error.message);
      process.exit(1);
    }
    return;
  }
  
  const validSeason = validateSeason(seasonName);
  if (!validSeason) {
    console.error(`❌ 无效的节气名称: ${seasonName}`);
    console.log('\n可用节气:', SEASONS.join(', '));
    process.exit(1);
  }
  
  const seasonName_zh = SEASON_NAMES[validSeason];
  console.log(`🚀 开始构建${seasonName_zh}(${validSeason})主题...\n`);
  
  try {
    execSync(`webpack --config webpack.multi.config.js --mode production --env TARGET=${validSeason}`, {
      stdio: 'inherit',
      cwd: process.cwd()
    });
    console.log(`\n✅ ${seasonName_zh}主题构建完成!`);
    console.log(`📁 输出目录: dist/${validSeason}/`);
  } catch (error) {
    console.error(`\n❌ ${seasonName_zh}主题构建失败:`, error.message);
    process.exit(1);
  }
}

/**
 * 启动开发服务器
 * @param {string} seasonName - 节气名称
 */
function startSeason(seasonName) {
  if (seasonName === 'all') {
    console.log('🚀 启动所有节气开发服务器...\n');
    console.log('注意: 由于端口限制,建议逐个启动节气开发服务器');
    console.log('可用命令: npm run start [节气名]\n');
    return;
  }
  
  const validSeason = validateSeason(seasonName);
  if (!validSeason) {
    console.error(`❌ 无效的节气名称: ${seasonName}`);
    console.log('\n可用节气:', SEASONS.join(', '));
    process.exit(1);
  }
  
  const seasonName_zh = SEASON_NAMES[validSeason];
  console.log(`🚀 启动${seasonName_zh}(${validSeason})开发服务器...\n`);
  
  try {
    execSync(`webpack serve --config webpack.multi.config.js --mode development --env TARGET=${validSeason}`, {
      stdio: 'inherit',
      cwd: process.cwd()
    });
  } catch (error) {
    console.error(`\n❌ ${seasonName_zh}开发服务器启动失败:`, error.message);
    process.exit(1);
  }
}

/**
 * 主函数
 */
function main() {
  const args = process.argv.slice(2);
  const command = args[0];
  const seasonName = args[1];
  
  // 显示帮助信息
  if (!command || command === 'help' || command === '-h' || command === '--help' || seasonName === 'help') {
    showHelp();
    return;
  }
  
  // 处理命令
  switch (command) {
    case 'build':
      if (!seasonName) {
        console.error('❌ 请指定要构建的节气名称');
        console.log('使用 npm run build help 查看帮助信息');
        process.exit(1);
      }
      buildSeason(seasonName);
      break;
      
    case 'start':
      if (!seasonName) {
        console.error('❌ 请指定要启动的节气名称');
        console.log('使用 npm run start help 查看帮助信息');
        process.exit(1);
      }
      startSeason(seasonName);
      break;
      
    default:
      console.error(`❌ 未知命令: ${command}`);
      console.log('可用命令: build, start');
      console.log('使用 npm run build help 查看帮助信息');
      process.exit(1);
  }
}

// 执行主函数
main();

public 目录

public/index.html(SPA 模板)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>二十四节气主题页面 - React版</title>
    <meta name="description" content="基于React的二十四节气主题页面,体验传统节气的独特魅力">
    <meta name="keywords" content="二十四节气,React,传统文化,主题页面,节气">
    <meta name="author" content="Face Webpack Team">
    
    <!-- Open Graph / Facebook -->
    <meta property="og:type" content="website">
    <meta property="og:url" content="https://face-webpack-react.com/">
    <meta property="og:title" content="二十四节气主题页面 - React版">
    <meta property="og:description" content="基于React的二十四节气主题页面,体验传统节气的独特魅力">
    
    <!-- Twitter -->
    <meta property="twitter:card" content="summary_large_image">
    <meta property="twitter:url" content="https://face-webpack-react.com/">
    <meta property="twitter:title" content="二十四节气主题页面 - React版">
    <meta property="twitter:description" content="基于React的二十四节气主题页面,体验传统节气的独特魅力">
    
    <style>
        /* 加载动画 */
        .loading {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: #f5f5f5;
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
            flex-direction: column;
        }
        
        .loading-spinner {
            width: 40px;
            height: 40px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #4CAF50;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-bottom: 16px;
        }
        
        .loading-text {
            color: #666;
            font-size: 14px;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        /* 隐藏加载动画 */
        .loading.hidden {
            display: none;
        }
    </style>
...</html>

public/season.html(单节气页面模板)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <meta name="description" content="<%= htmlWebpackPlugin.options.description %>">
    <meta name="keywords" content="二十四节气,<%= htmlWebpackPlugin.options.seasonId %>,传统文化,节气主题">
    <meta name="author" content="Face Webpack Team">
    
    <!-- Open Graph / Facebook -->
    <meta property="og:type" content="website">
    <meta property="og:url" content="<%= htmlWebpackPlugin.options.seasonData ? 'https://face-webpack-react.com/' + htmlWebpackPlugin.options.seasonId : '' %>">
    <meta property="og:title" content="<%= htmlWebpackPlugin.options.title %>">
    <meta property="og:description" content="<%= htmlWebpackPlugin.options.description %>">
    
    <!-- Twitter -->
    <meta property="twitter:card" content="summary_large_image">
    <meta property="twitter:url" content="<%= htmlWebpackPlugin.options.seasonData ? 'https://face-webpack-react.com/' + htmlWebpackPlugin.options.seasonId : '' %>">
    <meta property="twitter:title" content="<%= htmlWebpackPlugin.options.title %>">
    <meta property="twitter:description" content="<%= htmlWebpackPlugin.options.description %>">
    
    <style>
        /* 加载动画 */
        .loading {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: #f5f5f5;
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
            flex-direction: column;
        }
        
        .loading-spinner {
            width: 40px;
            height: 40px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #4CAF50;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-bottom: 16px;
        }
        
        .loading-text {
            color: #666;
            font-size: 14px;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        /* 隐藏加载动画 */
        .loading.hidden {
            display: none;
        }
    </style>
...</html>

src 目录

src/index.tsx

/**
 * 应用入口文件
 */

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles/global.css';

// 获取根元素
const container = document.getElementById('root');
if (!container) {
  throw new Error('找不到根元素 #root');
}

// 创建React根
const root = createRoot(container);

// 渲染应用
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

src/App.tsx

/**
 * 主应用组件
 * 使用React Router进行路由管理
 */

import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, useParams } from 'react-router-dom';
import { ThemeProvider } from '@/components/ThemeProvider';
import { SeasonPage } from '@/components/SeasonPage';
import { SeasonSelector } from '@/components/SeasonSelector';
import { useToast } from '@/components/Toast';
import { SEASONS } from '@/data/seasons';

// 节气详情页面组件
const SeasonDetailPage: React.FC = () => {
  const { seasonId } = useParams<{ seasonId: string }>();
  
  // 验证seasonId是否有效
  const isValidSeason = seasonId && SEASONS.some(s => s.id === seasonId);
  
  if (!isValidSeason) {
    return (
      <div className="error-page">
        <div className="container">
          <h1>节气不存在</h1>
          <p>找不到指定的节气:{seasonId}</p>
          <button 
            className="btn btn-primary"
            onClick={() => window.location.href = '/'}
          >
            返回首页
          </button>
        </div>
      </div>
    );
  }

  return <SeasonPage seasonId={seasonId} />;
};

// 首页组件
const HomePage: React.FC = () => {
  return (
    <div className="home-page">
      <div className="container">
        <header className="home-header">
          <h1>🌸 二十四节气主题页面</h1>
          <p>体验传统节气的独特魅力,感受四季变换的美好</p>
        </header>
        
        <SeasonSelector />
        
        <div className="home-content">
          <SeasonPage />
        </div>
      </div>
    </div>
  );
};

// 键盘快捷键处理
const KeyboardHandler: React.FC = () => {
  useEffect(() => {
    const handleKeydown = (event: KeyboardEvent) => {
      // 空格键随机切换主题
      if (event.code === 'Space' && !(event.target as Element)?.matches('input, textarea')) {
        event.preventDefault();
        // 这里需要访问主题上下文,暂时通过事件处理
        window.dispatchEvent(new CustomEvent('randomTheme'));
      }
      
      // ESC键返回首页
      if (event.code === 'Escape') {
        window.location.href = '/';
      }
    };

    document.addEventListener('keydown', handleKeydown);
    return () => document.removeEventListener('keydown', handleKeydown);
  }, []);

  return null;
};

// 主应用组件
const App: React.FC = () => {
  const { ToastContainer } = useToast();

  return (
    <ThemeProvider>
      <Router>
        <div className="app">
          <KeyboardHandler />
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/season/:seasonId" element={<SeasonDetailPage />} />
            <Route path="*" element={<HomePage />} />
          </Routes>
          <ToastContainer />
        </div>
      </Router>
    </ThemeProvider>
  );
};

export default App;

src/seasonEntry.tsx

/**
 * 节气专用入口文件
 * 用于生成独立的节气页面
 */

import React from 'react';
import { createRoot } from 'react-dom/client';
import { SeasonPage } from '@/components/SeasonPage';
import { ThemeProvider } from '@/components/ThemeProvider';
import { useToast } from '@/components/Toast';
import './styles/global.css';

// 从全局变量获取节气数据
const seasonData = (window as any).__SEASON_DATA__ || null;
const seasonId = seasonData?.id || 'lichun';

// 节气专用应用组件
const SeasonApp: React.FC = () => {
  const { ToastContainer } = useToast();

  return (
    <ThemeProvider>
      <div className="app">
        <SeasonPage seasonId={seasonId} />
        <ToastContainer />
      </div>
    </ThemeProvider>
  );
};

// 获取根元素
const container = document.getElementById('root');
if (!container) {
  throw new Error('找不到根元素 #root');
}

// 创建React根
const root = createRoot(container);

// 渲染应用
root.render(
  <React.StrictMode>
    <SeasonApp />
  </React.StrictMode>
);

src/styles/global.css

/* 全局样式文件 - React重构版 */

/* CSS变量定义 - 动态主题支持 */
:root {
  /* 默认主题颜色 */
  --primary-color: #4CAF50;
  --secondary-color: #8BC34A;
  --background-color: #F1F8E9;
  --text-color: #2E7D32;
  --accent-color: #C8E6C9;
  --gradient: linear-gradient(135deg, #4CAF50 0%, #8BC34A 100%);
  
  /* 字体设置 */
  --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  --font-family-serif: 'Times New Roman', '宋体', serif;
  
  /* 间距设置 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;
  --spacing-xxl: 48px;
  
  /* 圆角设置 */
  --border-radius-sm: 4px;
  --border-radius-md: 8px;
  --border-radius-lg: 12px;
  --border-radius-xl: 16px;
  
  /* 阴影设置 */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
  --shadow-md: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
  --shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
  
  /* 过渡动画 */
  --transition-fast: 0.2s ease-in-out;
  --transition-normal: 0.3s ease-in-out;
  --transition-slow: 0.5s ease-in-out;
}

/* 基础样式重置 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-size: 16px;
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-family);
  line-height: 1.6;
  color: var(--text-color);
  background: var(--background-color);
  transition: all var(--transition-normal);
  min-height: 100vh;
}

/* 应用容器 */
.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* 容器样式 */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--spacing-md);
}

.container-fluid {
  width: 100%;
  padding: 0 var(--spacing-md);
}

/* 布局样式 */
.flex {
  display: flex;
}

.flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.flex-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.flex-column {
  display: flex;
  flex-direction: column;
}

.flex-wrap {
  flex-wrap: wrap;
}

/* 按钮样式 */
.btn {
  display: inline-block;
  padding: var(--spacing-sm) var(--spacing-lg);
  border: none;
  border-radius: var(--border-radius-md);
  font-size: 1rem;
  font-weight: 500;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  transition: all var(--transition-fast);
  box-shadow: var(--shadow-sm);
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.btn-primary {
  background: var(--gradient);
  color: white;
}

.btn-secondary {
  background: var(--secondary-color);
  color: white;
}

.btn-outline {
  background: transparent;
  color: var(--primary-color);
  border: 2px solid var(--primary-color);
}

.btn-outline:hover {
  background: var(--primary-color);
  color: white;
}

/* 卡片样式 */
.season-card {
  background: white;
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--spacing-lg);
  margin: var(--spacing-md) 0;
  transition: all var(--transition-normal);
}

.season-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
}

.season-card h3 {
  color: var(--primary-color);
  margin-bottom: var(--spacing-md);
  font-size: 1.3rem;
}

.card-content {
  color: #666;
  line-height: 1.8;
}

/* 首页样式 */
.home-page {
  min-height: 100vh;
  background: var(--background-color);
}

.home-header {
  text-align: center;
  padding: var(--spacing-xxl) 0;
  background: var(--gradient);
  color: white;
  margin-bottom: var(--spacing-xxl);
}

.home-header h1 {
  font-size: 3rem;
  margin-bottom: var(--spacing-md);
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}

.home-header p {
  font-size: 1.2rem;
  opacity: 0.9;
}

/* 节气选择器样式 */
.season-selector {
  background: white;
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-md);
  margin-bottom: var(--spacing-xxl);
  overflow: hidden;
}

.selector-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--spacing-lg);
  background: var(--accent-color);
}

.selector-header h2 {
  color: var(--text-color);
  margin: 0;
}

.selector-content {
  padding: var(--spacing-lg);
}

.category-tabs {
  display: flex;
  gap: var(--spacing-sm);
  margin-bottom: var(--spacing-lg);
  flex-wrap: wrap;
}

.category-tab {
  padding: var(--spacing-sm) var(--spacing-md);
  border: 2px solid var(--primary-color);
  background: transparent;
  color: var(--primary-color);
  border-radius: var(--border-radius-md);
  cursor: pointer;
  transition: all var(--transition-fast);
  font-weight: 500;
}

.category-tab:hover,
.category-tab.active {
  background: var(--primary-color);
  color: white;
}

.seasons-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--spacing-md);
}

.season-item {
  border: 2px solid transparent;
  border-radius: var(--border-radius-md);
  cursor: pointer;
  transition: all var(--transition-fast);
  overflow: hidden;
}

.season-item:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.season-item.active {
  border-color: var(--primary-color);
  box-shadow: var(--shadow-md);
}

.season-preview {
  background: white;
}

.season-color-bar {
  height: 4px;
  width: 100%;
}

.season-info {
  padding: var(--spacing-md);
}

.season-info h4 {
  color: var(--text-color);
  margin-bottom: var(--spacing-xs);
  font-size: 1.1rem;
}

.season-info p {
  color: #666;
  font-size: 0.9rem;
  margin-bottom: var(--spacing-xs);
}

.season-date {
  color: #999;
  font-size: 0.8rem;
}

/* 节气页面样式 */
.season-page {
  min-height: 100vh;
  background: var(--background-color);
  transition: all var(--transition-slow);
}

.season-header {
  background: var(--gradient);
  color: white;
  padding: var(--spacing-xxl) 0;
  text-align: center;
  position: relative;
  overflow: hidden;
}

.season-header::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="50" cy="10" r="0.5" fill="rgba(255,255,255,0.05)"/><circle cx="10" cy="60" r="0.5" fill="rgba(255,255,255,0.05)"/><circle cx="90" cy="40" r="0.5" fill="rgba(255,255,255,0.05)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
  opacity: 0.3;
}

.season-title {
  font-size: 3rem;
  font-weight: bold;
  margin-bottom: var(--spacing-md);
  position: relative;
  z-index: 1;
}

.season-subtitle {
  font-size: 1.2rem;
  opacity: 0.9;
  position: relative;
  z-index: 1;
}

.season-date {
  font-size: 1rem;
  opacity: 0.8;
  position: relative;
  z-index: 1;
}

.season-content {
  padding: var(--spacing-xxl) 0;
}

.season-info {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--spacing-lg);
  margin-bottom: var(--spacing-xxl);
}

.season-poetry {
  background: var(--accent-color);
  border-radius: var(--border-radius-lg);
  padding: var(--spacing-lg);
  text-align: center;
  font-style: italic;
  font-size: 1.1rem;
  color: var(--text-color);
  margin: var(--spacing-lg) 0;
  border-left: 4px solid var(--primary-color);
}

.color-palette {
  display: flex;
  gap: var(--spacing-md);
  justify-content: center;
  flex-wrap: wrap;
}

.color-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-sm);
}

.color-swatch {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  border: 3px solid white;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: all var(--transition-fast);
}

.color-swatch:hover {
  transform: scale(1.1);
  box-shadow: var(--shadow-md);
}

.action-buttons {
  display: flex;
  gap: var(--spacing-md);
  justify-content: center;
  margin-top: var(--spacing-xl);
  flex-wrap: wrap;
}

.season-footer {
  background: rgba(var(--primary-color-rgb, 76, 175, 80), 0.1);
  padding: var(--spacing-xl) 0;
  text-align: center;
  color: var(--text-color);
  margin-top: auto;
}

/* 错误页面样式 */
.error-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--background-color);
}

.error-page .container {
  text-align: center;
}

.error-page h1 {
  color: var(--primary-color);
  margin-bottom: var(--spacing-md);
}

.error-page p {
  color: #666;
  margin-bottom: var(--spacing-lg);
}

/* 动画效果 */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

.fade-in {
  animation: fadeIn 0.6s ease-out;
}

.slide-in {
  animation: slideIn 0.6s ease-out;
}

/* 主题切换动画 */
.theme-transition {
  transition: all var(--transition-slow);
}

/* Toast 提示样式 */
.toast-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 10000;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.toast {
  background: white;
  border-radius: var(--border-radius-md);
  box-shadow: var(--shadow-lg);
  padding: 0;
  min-width: 300px;
  max-width: 400px;
  transform: translateX(100%);
  transition: all var(--transition-normal);
  border-left: 4px solid var(--primary-color);
}

.toast-visible {
  transform: translateX(0);
}

.toast-hidden {
  transform: translateX(100%);
}

.toast-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-md) var(--spacing-lg);
}

.toast-message {
  color: var(--text-color);
  font-size: 14px;
  line-height: 1.4;
  flex: 1;
}

.toast-close {
  background: none;
  border: none;
  color: #999;
  font-size: 18px;
  cursor: pointer;
  padding: 0;
  margin-left: var(--spacing-sm);
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  transition: all var(--transition-fast);
}

.toast-close:hover {
  background: #f5f5f5;
  color: #666;
}

.toast-success {
  border-left-color: #4CAF50;
}

.toast-error {
  border-left-color: #F44336;
}

.toast-info {
  border-left-color: var(--primary-color);
}

/* 响应式设计 */
@media (max-width: 768px) {
  .container {
    padding: 0 var(--spacing-sm);
  }
  
  .season-title {
    font-size: 2rem;
  }
  
  .season-subtitle {
    font-size: 1rem;
  }
  
  .season-info {
    grid-template-columns: 1fr;
    gap: var(--spacing-md);
  }
  
  .season-header {
    padding: var(--spacing-xl) 0;
  }
  
  .season-content {
    padding: var(--spacing-xl) 0;
  }
  
  .seasons-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  }
  
  .category-tabs {
    justify-content: center;
  }
  
  .action-buttons {
    flex-direction: column;
    align-items: center;
  }
}

@media (max-width: 480px) {
  .season-title {
    font-size: 1.5rem;
  }
  
  .season-card {
    padding: var(--spacing-md);
  }
  
  .season-poetry {
    padding: var(--spacing-md);
    font-size: 1rem;
  }
  
  .color-swatch {
    width: 40px;
    height: 40px;
  }
  
  .seasons-grid {
    grid-template-columns: 1fr;
  }
}

src/hooks/useTheme.ts

/**
 * 主题管理Hook
 * 提供主题切换、应用和状态管理功能
 */

import { useState, useEffect, useCallback } from 'react';
import { SeasonData, getSeasonById, getRandomSeason, getCurrentSeason } from '@/data/seasons';

export interface ThemeState {
  currentSeason: SeasonData;
  isLoading: boolean;
  error: string | null;
}

export const useTheme = () => {
  const [themeState, setThemeState] = useState<ThemeState>(() => {
    // 优先从全局变量获取节气数据(节气专用页面)
    const globalSeasonData = (window as any).__SEASON_DATA__;
    if (globalSeasonData) {
      return {
        currentSeason: globalSeasonData,
        isLoading: false,
        error: null
      };
    }

    // 从URL参数获取初始主题
    const urlParams = new URLSearchParams(window.location.search);
    const themeFromUrl = urlParams.get('theme');
    
    if (themeFromUrl) {
      const season = getSeasonById(themeFromUrl);
      if (season) {
        return {
          currentSeason: season,
          isLoading: false,
          error: null
        };
      }
    }

    // 从localStorage获取
    const savedTheme = localStorage.getItem('currentSeasonTheme');
    if (savedTheme) {
      const season = getSeasonById(savedTheme);
      if (season) {
        return {
          currentSeason: season,
          isLoading: false,
          error: null
        };
      }
    }

    // 默认使用当前节气
    return {
      currentSeason: getCurrentSeason(),
      isLoading: false,
      error: null
    };
  });

  // 应用主题到DOM
  const applyTheme = useCallback((season: SeasonData) => {
    const root = document.documentElement;
    const { theme } = season;

    // 更新CSS变量
    root.style.setProperty('--primary-color', theme.primaryColor);
    root.style.setProperty('--secondary-color', theme.secondaryColor);
    root.style.setProperty('--background-color', theme.backgroundColor);
    root.style.setProperty('--text-color', theme.textColor);
    root.style.setProperty('--accent-color', theme.accentColor);
    root.style.setProperty('--gradient', theme.gradient);

    // 更新自定义样式
    if (season.customStyle) {
      if (season.customStyle.fontFamily) {
        root.style.setProperty('--font-family', season.customStyle.fontFamily);
      }
      if (season.customStyle.borderRadius) {
        root.style.setProperty('--border-radius-md', season.customStyle.borderRadius);
      }
    }

    // 更新页面标题和meta标签
    document.title = `${season.name} - 二十四节气主题页面`;
    
    const descriptionMeta = document.querySelector('meta[name="description"]');
    if (descriptionMeta) {
      descriptionMeta.setAttribute('content', `${season.name} - ${season.description}`);
    }

    // 更新body类名
    document.body.className = `theme-${season.id} theme-transition`;

    // 保存到localStorage
    localStorage.setItem('currentSeasonTheme', season.id);
  }, []);

  // 切换主题
  const setTheme = useCallback((seasonId: string) => {
    const season = getSeasonById(seasonId);
    if (!season) {
      setThemeState(prev => ({
        ...prev,
        error: `主题 ${seasonId} 不存在`
      }));
      return;
    }

    setThemeState(prev => ({
      ...prev,
      currentSeason: season,
      error: null
    }));

    applyTheme(season);
  }, [applyTheme]);

  // 随机切换主题
  const setRandomTheme = useCallback(() => {
    const randomSeason = getRandomSeason();
    setTheme(randomSeason.id);
  }, [setTheme]);

  // 初始化时应用主题
  useEffect(() => {
    applyTheme(themeState.currentSeason);
  }, [applyTheme, themeState.currentSeason]);

  // 监听URL变化
  useEffect(() => {
    const handleUrlChange = () => {
      const urlParams = new URLSearchParams(window.location.search);
      const themeFromUrl = urlParams.get('theme');
      
      if (themeFromUrl && themeFromUrl !== themeState.currentSeason.id) {
        setTheme(themeFromUrl);
      }
    };

    window.addEventListener('popstate', handleUrlChange);
    return () => window.removeEventListener('popstate', handleUrlChange);
  }, [setTheme, themeState.currentSeason.id]);

  return {
    ...themeState,
    setTheme,
    setRandomTheme,
    applyTheme
  };
};

src/data/seasons.ts(较长,完整收录)

/**
 * 二十四节气配置数据
 * 使用TypeScript类型定义,提供更好的类型安全
 */

export interface SeasonTheme {
  primaryColor: string;
  secondaryColor: string;
  backgroundColor: string;
  textColor: string;
  accentColor: string;
  gradient: string;
}

export interface SeasonImages {
  background?: string;
  icon?: string;
  pattern?: string;
}

export interface SeasonCustomStyle {
  fontFamily?: string;
  borderRadius?: string;
  animation?: string;
}

export interface SeasonData {
  id: string;
  name: string;
  englishName: string;
  date: string;
  description: string;
  theme: SeasonTheme;
  images?: SeasonImages;
  poetry: string;
  customStyle?: SeasonCustomStyle;
  category: 'spring' | 'summer' | 'autumn' | 'winter';
  order: number;
}

export const SEASONS: SeasonData[] = [
  // 春季
  {
    id: 'lichun',
    name: '立春',
    englishName: 'Beginning of Spring',
    date: '2024-02-04',
    description: '立春是二十四节气中的第一个节气,标志着春天的开始。',
    theme: {
      primaryColor: '#4CAF50',
      secondaryColor: '#8BC34A',
      backgroundColor: '#F1F8E9',
      textColor: '#2E7D32',
      accentColor: '#C8E6C9',
      gradient: 'linear-gradient(135deg, #4CAF50 0%, #8BC34A 100%)'
    },
    poetry: '立春一日,百草回芽。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '8px',
      animation: 'springBounce'
    },
    category: 'spring',
    order: 1
  },
  {
    id: 'yushui',
    name: '雨水',
    englishName: 'Rain Water',
    date: '2024-02-19',
    description: '雨水节气,降雨开始,雨量渐增。',
    theme: {
      primaryColor: '#2196F3',
      secondaryColor: '#64B5F6',
      backgroundColor: '#E3F2FD',
      textColor: '#1565C0',
      accentColor: '#BBDEFB',
      gradient: 'linear-gradient(135deg, #2196F3 0%, #64B5F6 100%)'
    },
    poetry: '雨水时节,万物复苏。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '12px',
      animation: 'rainDrop'
    },
    category: 'spring',
    order: 2
  },
  {
    id: 'jingzhe',
    name: '惊蛰',
    englishName: 'Awakening of Insects',
    date: '2024-03-05',
    description: '惊蛰时节,春雷始鸣,惊醒蛰伏于地下冬眠的昆虫。',
    theme: {
      primaryColor: '#FF9800',
      secondaryColor: '#FFB74D',
      backgroundColor: '#FFF3E0',
      textColor: '#E65100',
      accentColor: '#FFE0B2',
      gradient: 'linear-gradient(135deg, #FF9800 0%, #FFB74D 100%)'
    },
    poetry: '惊蛰雷动,万物复苏。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '6px',
      animation: 'thunderShake'
    },
    category: 'spring',
    order: 3
  },
  {
    id: 'chunfen',
    name: '春分',
    englishName: 'Spring Equinox',
    date: '2024-03-20',
    description: '春分时节,昼夜平分,春暖花开。',
    theme: {
      primaryColor: '#E91E63',
      secondaryColor: '#F48FB1',
      backgroundColor: '#FCE4EC',
      textColor: '#AD1457',
      accentColor: '#F8BBD9',
      gradient: 'linear-gradient(135deg, #E91E63 0%, #F48FB1 100%)'
    },
    poetry: '春分时节,百花齐放。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '10px',
      animation: 'flowerBloom'
    },
    category: 'spring',
    order: 4
  },
  {
    id: 'qingming',
    name: '清明',
    englishName: 'Pure Brightness',
    date: '2024-04-04',
    description: '清明时节,天气清和,草木茂盛。',
    theme: {
      primaryColor: '#9C27B0',
      secondaryColor: '#BA68C8',
      backgroundColor: '#F3E5F5',
      textColor: '#6A1B9A',
      accentColor: '#E1BEE7',
      gradient: 'linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%)'
    },
    poetry: '清明时节雨纷纷,路上行人欲断魂。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '8px',
      animation: 'gentleRain'
    },
    category: 'spring',
    order: 5
  },
  {
    id: 'guyu',
    name: '谷雨',
    englishName: 'Grain Rain',
    date: '2024-04-19',
    description: '谷雨时节,雨生百谷,春意盎然。',
    theme: {
      primaryColor: '#673AB7',
      secondaryColor: '#9575CD',
      backgroundColor: '#EDE7F6',
      textColor: '#4527A0',
      accentColor: '#D1C4E9',
      gradient: 'linear-gradient(135deg, #673AB7 0%, #9575CD 100%)'
    },
    poetry: '谷雨时节,万物生长。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '12px',
      animation: 'grainGrow'
    },
    category: 'spring',
    order: 6
  },
  // 夏季
  {
    id: 'lixia',
    name: '立夏',
    englishName: 'Beginning of Summer',
    date: '2024-05-05',
    description: '立夏时节,夏季开始,万物繁茂。',
    theme: {
      primaryColor: '#F44336',
      secondaryColor: '#EF5350',
      backgroundColor: '#FFEBEE',
      textColor: '#C62828',
      accentColor: '#FFCDD2',
      gradient: 'linear-gradient(135deg, #F44336 0%, #EF5350 100%)'
    },
    poetry: '立夏时节,绿树成荫。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '6px',
      animation: 'summerHeat'
    },
    category: 'summer',
    order: 7
  },
  {
    id: 'xiaoman',
    name: '小满',
    englishName: 'Grain Buds',
    date: '2024-05-20',
    description: '小满时节,麦类等夏熟作物籽粒开始饱满。',
    theme: {
      primaryColor: '#FF5722',
      secondaryColor: '#FF8A65',
      backgroundColor: '#FBE9E7',
      textColor: '#D84315',
      accentColor: '#FFCCBC',
      gradient: 'linear-gradient(135deg, #FF5722 0%, #FF8A65 100%)'
    },
    poetry: '小满时节,麦穗渐满。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '10px',
      animation: 'grainRipen'
    },
    category: 'summer',
    order: 8
  },
  {
    id: 'mangzhong',
    name: '芒种',
    englishName: 'Grain in Ear',
    date: '2024-06-05',
    description: '芒种时节,有芒的麦子快收,有芒的稻子可种。',
    theme: {
      primaryColor: '#795548',
      secondaryColor: '#A1887F',
      backgroundColor: '#EFEBE9',
      textColor: '#4E342E',
      accentColor: '#D7CCC8',
      gradient: 'linear-gradient(135deg, #795548 0%, #A1887F 100%)'
    },
    poetry: '芒种时节,农事繁忙。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '8px',
      animation: 'harvestWork'
    },
    category: 'summer',
    order: 9
  },
  {
    id: 'xiazhi',
    name: '夏至',
    englishName: 'Summer Solstice',
    date: '2024-06-21',
    description: '夏至时节,白昼最长,夜晚最短。',
    theme: {
      primaryColor: '#FFC107',
      secondaryColor: '#FFD54F',
      backgroundColor: '#FFFDE7',
      textColor: '#F57F17',
      accentColor: '#FFF9C4',
      gradient: 'linear-gradient(135deg, #FFC107 0%, #FFD54F 100%)'
    },
    poetry: '夏至时节,日长夜短。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '12px',
      animation: 'sunShine'
    },
    category: 'summer',
    order: 10
  },
  {
    id: 'xiaoshu',
    name: '小暑',
    englishName: 'Minor Heat',
    date: '2024-07-06',
    description: '小暑时节,天气开始炎热,但还不是最热的时候。',
    theme: {
      primaryColor: '#FF9800',
      secondaryColor: '#FFB74D',
      backgroundColor: '#FFF3E0',
      textColor: '#E65100',
      accentColor: '#FFE0B2',
      gradient: 'linear-gradient(135deg, #FF9800 0%, #FFB74D 100%)'
    },
    poetry: '小暑时节,热浪初起。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '6px',
      animation: 'heatWave'
    },
    category: 'summer',
    order: 11
  },
  {
    id: 'dashu',
    name: '大暑',
    englishName: 'Major Heat',
    date: '2024-07-22',
    description: '大暑时节,一年中最热的时期。',
    theme: {
      primaryColor: '#F44336',
      secondaryColor: '#EF5350',
      backgroundColor: '#FFEBEE',
      textColor: '#C62828',
      accentColor: '#FFCDD2',
      gradient: 'linear-gradient(135deg, #F44336 0%, #EF5350 100%)'
    },
    poetry: '大暑时节,酷热难当。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '10px',
      animation: 'intenseHeat'
    },
    category: 'summer',
    order: 12
  },
  // 秋季
  {
    id: 'liqiu',
    name: '立秋',
    englishName: 'Beginning of Autumn',
    date: '2024-08-07',
    description: '立秋时节,秋季开始,暑去凉来。',
    theme: {
      primaryColor: '#607D8B',
      secondaryColor: '#90A4AE',
      backgroundColor: '#ECEFF1',
      textColor: '#37474F',
      accentColor: '#CFD8DC',
      gradient: 'linear-gradient(135deg, #607D8B 0%, #90A4AE 100%)'
    },
    poetry: '立秋时节,天高气爽。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '8px',
      animation: 'autumnBreeze'
    },
    category: 'autumn',
    order: 13
  },
  {
    id: 'chushu',
    name: '处暑',
    englishName: 'End of Heat',
    date: '2024-08-22',
    description: '处暑时节,暑气渐消,秋意渐浓。',
    theme: {
      primaryColor: '#3F51B5',
      secondaryColor: '#7986CB',
      backgroundColor: '#E8EAF6',
      textColor: '#283593',
      accentColor: '#C5CAE9',
      gradient: 'linear-gradient(135deg, #3F51B5 0%, #7986CB 100%)'
    },
    poetry: '处暑时节,暑气渐消。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '12px',
      animation: 'coolDown'
    },
    category: 'autumn',
    order: 14
  },
  {
    id: 'bailu',
    name: '白露',
    englishName: 'White Dew',
    date: '2024-09-07',
    description: '白露时节,天气转凉,露凝而白。',
    theme: {
      primaryColor: '#00BCD4',
      secondaryColor: '#4DD0E1',
      backgroundColor: '#E0F2F1',
      textColor: '#00695C',
      accentColor: '#B2DFDB',
      gradient: 'linear-gradient(135deg, #00BCD4 0%, #4DD0E1 100%)'
    },
    poetry: '白露时节,露凝而白。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '6px',
      animation: 'dewForm'
    },
    category: 'autumn',
    order: 15
  },
  {
    id: 'qiufen',
    name: '秋分',
    englishName: 'Autumn Equinox',
    date: '2024-09-22',
    description: '秋分时节,昼夜平分,秋高气爽。',
    theme: {
      primaryColor: '#009688',
      secondaryColor: '#4DB6AC',
      backgroundColor: '#E0F2F1',
      textColor: '#004D40',
      accentColor: '#B2DFDB',
      gradient: 'linear-gradient(135deg, #009688 0%, #4DB6AC 100%)'
    },
    poetry: '秋分时节,昼夜平分。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '10px',
      animation: 'balance'
    },
    category: 'autumn',
    order: 16
  },
  {
    id: 'hanlu',
    name: '寒露',
    englishName: 'Cold Dew',
    date: '2024-10-08',
    description: '寒露时节,露气寒冷,将凝结也。',
    theme: {
      primaryColor: '#795548',
      secondaryColor: '#A1887F',
      backgroundColor: '#EFEBE9',
      textColor: '#4E342E',
      accentColor: '#D7CCC8',
      gradient: 'linear-gradient(135deg, #795548 0%, #A1887F 100%)'
    },
    poetry: '寒露时节,露气寒冷。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '8px',
      animation: 'coldDew'
    },
    category: 'autumn',
    order: 17
  },
  {
    id: 'shuangjiang',
    name: '霜降',
    englishName: 'Frost\'s Descent',
    date: '2024-10-23',
    description: '霜降时节,天气渐冷,开始有霜。',
    theme: {
      primaryColor: '#607D8B',
      secondaryColor: '#90A4AE',
      backgroundColor: '#ECEFF1',
      textColor: '#37474F',
      accentColor: '#CFD8DC',
      gradient: 'linear-gradient(135deg, #607D8B 0%, #90A4AE 100%)'
    },
    poetry: '霜降时节,霜花满地。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '12px',
      animation: 'frostForm'
    },
    category: 'autumn',
    order: 18
  },
  // 冬季
  {
    id: 'lidong',
    name: '立冬',
    englishName: 'Beginning of Winter',
    date: '2024-11-07',
    description: '立冬时节,冬季开始,万物收藏。',
    theme: {
      primaryColor: '#3F51B5',
      secondaryColor: '#7986CB',
      backgroundColor: '#E8EAF6',
      textColor: '#283593',
      accentColor: '#C5CAE9',
      gradient: 'linear-gradient(135deg, #3F51B5 0%, #7986CB 100%)'
    },
    poetry: '立冬时节,万物收藏。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '6px',
      animation: 'winterStart'
    },
    category: 'winter',
    order: 19
  },
  {
    id: 'xiaoxue',
    name: '小雪',
    englishName: 'Minor Snow',
    date: '2024-11-22',
    description: '小雪时节,开始下雪,雪量不大。',
    theme: {
      primaryColor: '#E1F5FE',
      secondaryColor: '#B3E5FC',
      backgroundColor: '#F3F9FF',
      textColor: '#01579B',
      accentColor: '#E1F5FE',
      gradient: 'linear-gradient(135deg, #E1F5FE 0%, #B3E5FC 100%)'
    },
    poetry: '小雪时节,雪花纷飞。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '10px',
      animation: 'snowFall'
    },
    category: 'winter',
    order: 20
  },
  {
    id: 'daxue',
    name: '大雪',
    englishName: 'Major Snow',
    date: '2024-12-07',
    description: '大雪时节,雪量增大,地面积雪。',
    theme: {
      primaryColor: '#FFFFFF',
      secondaryColor: '#F5F5F5',
      backgroundColor: '#FAFAFA',
      textColor: '#424242',
      accentColor: '#EEEEEE',
      gradient: 'linear-gradient(135deg, #FFFFFF 0%, #F5F5F5 100%)'
    },
    poetry: '大雪时节,银装素裹。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '8px',
      animation: 'heavySnow'
    },
    category: 'winter',
    order: 21
  },
  {
    id: 'dongzhi',
    name: '冬至',
    englishName: 'Winter Solstice',
    date: '2024-12-21',
    description: '冬至时节,白昼最短,夜晚最长。',
    theme: {
      primaryColor: '#212121',
      secondaryColor: '#424242',
      backgroundColor: '#F5F5F5',
      textColor: '#000000',
      accentColor: '#E0E0E0',
      gradient: 'linear-gradient(135deg, #212121 0%, #424242 100%)'
    },
    poetry: '冬至时节,日短夜长。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '12px',
      animation: 'longNight'
    },
    category: 'winter',
    order: 22
  },
  {
    id: 'xiaohan',
    name: '小寒',
    englishName: 'Minor Cold',
    date: '2025-01-05',
    description: '小寒时节,天气寒冷,但还不是最冷的时候。',
    theme: {
      primaryColor: '#37474F',
      secondaryColor: '#546E7A',
      backgroundColor: '#ECEFF1',
      textColor: '#263238',
      accentColor: '#CFD8DC',
      gradient: 'linear-gradient(135deg, #37474F 0%, #546E7A 100%)'
    },
    poetry: '小寒时节,寒气逼人。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '6px',
      animation: 'coldWind'
    },
    category: 'winter',
    order: 23
  },
  {
    id: 'dahan',
    name: '大寒',
    englishName: 'Major Cold',
    date: '2025-01-20',
    description: '大寒时节,一年中最冷的时期。',
    theme: {
      primaryColor: '#263238',
      secondaryColor: '#37474F',
      backgroundColor: '#ECEFF1',
      textColor: '#000000',
      accentColor: '#CFD8DC',
      gradient: 'linear-gradient(135deg, #263238 0%, #37474F 100%)'
    },
    poetry: '大寒时节,天寒地冻。',
    customStyle: {
      fontFamily: 'serif',
      borderRadius: '10px',
      animation: 'freeze'
    },
    category: 'winter',
    order: 24
  }
];

// 工具函数
export const getSeasonById = (id: string): SeasonData | undefined => {
  return SEASONS.find(season => season.id === id);
};

export const getSeasonsByCategory = (category: SeasonData['category']): SeasonData[] => {
  return SEASONS.filter(season => season.category === category);
};

export const getRandomSeason = (): SeasonData => {
  const randomIndex = Math.floor(Math.random() * SEASONS.length);
  return SEASONS[randomIndex];
};

export const getCurrentSeason = (): SeasonData => {
  const now = new Date();
  const currentMonth = now.getMonth() + 1;
  const currentDay = now.getDate();
  
  // 简化的当前节气判断逻辑
  if (currentMonth >= 2 && currentMonth <= 4) {
    return getSeasonsByCategory('spring')[0];
  } else if (currentMonth >= 5 && currentMonth <= 7) {
    return getSeasonsByCategory('summer')[0];
  } else if (currentMonth >= 8 && currentMonth <= 10) {
    return getSeasonsByCategory('autumn')[0];
  } else {
    return getSeasonsByCategory('winter')[0];
  }
};

src/components/ThemeProvider.tsx

/**
 * 主题提供者组件
 * 为整个应用提供主题上下文
 */

import React, { createContext, useContext, ReactNode } from 'react';
import { useTheme, ThemeState } from '@/hooks/useTheme';
import { SeasonData } from '@/data/seasons';

interface ThemeContextType extends ThemeState {
  setTheme: (seasonId: string) => void;
  setRandomTheme: () => void;
  applyTheme: (season: SeasonData) => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  const theme = useTheme();

  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useThemeContext = (): ThemeContextType => {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useThemeContext must be used within a ThemeProvider');
  }
  return context;
};

src/components/SeasonPage.tsx

/**
 * 节气页面组件
 * 展示单个节气的详细信息
 */

import React, { useEffect } from 'react';
import { useThemeContext } from './ThemeProvider';
import { SeasonCard } from './SeasonCard';
import { ColorPalette } from './ColorPalette';
import { ActionButtons } from './ActionButtons';

interface SeasonPageProps {
  seasonId?: string;
}

export const SeasonPage: React.FC<SeasonPageProps> = ({ seasonId }) => {
  const { currentSeason, setTheme, setRandomTheme, error } = useThemeContext();

  // 如果指定了seasonId且与当前主题不同,则切换主题
  useEffect(() => {
    if (seasonId && seasonId !== currentSeason.id) {
      setTheme(seasonId);
    }
  }, [seasonId, currentSeason.id, setTheme]);

  if (error) {
    return (
      <div className="error-page">
        <div className="container">
          <h1>页面加载失败</h1>
          <p>{error}</p>
          <button 
            className="btn btn-primary" 
            onClick={() => window.location.reload()}
          >
            刷新页面
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="season-page">
      {/* 页面头部 */}
      <header className="season-header">
        <div className="container">
          <h1 className="season-title">{currentSeason.name}</h1>
          <p className="season-subtitle">{currentSeason.englishName}</p>
          <p className="season-date">{currentSeason.date}</p>
        </div>
      </header>

      {/* 页面内容 */}
      <main className="season-content">
        <div className="container">
          {/* 节气信息卡片 */}
          <div className="season-info">
            <SeasonCard
              title="节气简介"
              content={currentSeason.description}
            />
            
            <SeasonCard
              title="节气日期"
              content={currentSeason.date}
            />
            
            <SeasonCard
              title="英文名称"
              content={currentSeason.englishName}
            />
          </div>

          {/* 节气诗词 */}
          <div className="season-poetry">
            <p>"{currentSeason.poetry}"</p>
          </div>

          {/* 主题色彩 */}
          <SeasonCard title="主题色彩">
            <ColorPalette theme={currentSeason.theme} />
          </SeasonCard>

          {/* 操作按钮 */}
          <ActionButtons
            onRandomTheme={setRandomTheme}
            currentSeason={currentSeason}
          />
        </div>
      </main>

      {/* 页面底部 */}
      <footer className="season-footer">
        <div className="container">
          <p>&copy; 2024 二十四节气主题页面 - {currentSeason.name}</p>
        </div>
      </footer>
    </div>
  );
};

src/components/SeasonSelector.tsx

/**
 * 节气选择器组件
 * 用于展示所有节气并支持切换
 */

import React, { useState } from 'react';
import { useThemeContext } from './ThemeProvider';
import { SEASONS, getSeasonsByCategory } from '@/data/seasons';
import { SeasonData } from '@/data/seasons';

export const SeasonSelector: React.FC = () => {
  const { currentSeason, setTheme } = useThemeContext();
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [isOpen, setIsOpen] = useState(false);

  const categories = [
    { id: 'all', name: '全部', seasons: SEASONS },
    { id: 'spring', name: '春季', seasons: getSeasonsByCategory('spring') },
    { id: 'summer', name: '夏季', seasons: getSeasonsByCategory('summer') },
    { id: 'autumn', name: '秋季', seasons: getSeasonsByCategory('autumn') },
    { id: 'winter', name: '冬季', seasons: getSeasonsByCategory('winter') }
  ];

  const currentCategory = categories.find(cat => cat.id === selectedCategory);
  const displaySeasons = currentCategory?.seasons || SEASONS;

  const handleSeasonSelect = (season: SeasonData) => {
    setTheme(season.id);
    setIsOpen(false);
  };

  return (
    <div className="season-selector">
      <div className="selector-header">
        <h2>选择节气主题</h2>
        <button 
          className="btn btn-outline"
          onClick={() => setIsOpen(!isOpen)}
        >
          {isOpen ? '收起' : '展开'}
        </button>
      </div>

      {isOpen && (
        <div className="selector-content">
          {/* 分类选择 */}
          <div className="category-tabs">
            {categories.map(category => (
              <button
                key={category.id}
                className={`category-tab ${selectedCategory === category.id ? 'active' : ''}`}
                onClick={() => setSelectedCategory(category.id)}
              >
                {category.name}
              </button>
            ))}
          </div>

          {/* 节气网格 */}
          <div className="seasons-grid">
            {displaySeasons.map(season => (
              <div
                key={season.id}
                className={`season-item ${currentSeason.id === season.id ? 'active' : ''}`}
                onClick={() => handleSeasonSelect(season)}
                style={{
                  '--primary-color': season.theme.primaryColor,
                  '--secondary-color': season.theme.secondaryColor
                } as React.CSSProperties}
              >
                <div className="season-preview">
                  <div 
                    className="season-color-bar"
                    style={{ background: season.theme.gradient }}
                  />
                  <div className="season-info">
                    <h4>{season.name}</h4>
                    <p>{season.englishName}</p>
                    <span className="season-date">{season.date}</span>
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

src/components/ColorPalette.tsx

/**
 * 色彩调色板组件
 */

import React from 'react';
import { SeasonTheme } from '@/data/seasons';

interface ColorPaletteProps {
  theme: SeasonTheme;
}

export const ColorPalette: React.FC<ColorPaletteProps> = ({ theme }) => {
  const colors = [
    { name: '主色调', color: theme.primaryColor },
    { name: '辅助色', color: theme.secondaryColor },
    { name: '强调色', color: theme.accentColor }
  ];

  return (
    <div className="color-palette">
      {colors.map(({ name, color }) => (
        <div key={name} className="color-item">
          <div 
            className="color-swatch" 
            style={{ backgroundColor: color }}
            title={`${name}: ${color}`}
          />
          <span>{name}</span>
        </div>
      ))}
    </div>
  );
};

src/components/ActionButtons.tsx

/**
 * 操作按钮组件
 */

import React from 'react';
import { SeasonData } from '@/data/seasons';
import { useToast } from './Toast';

interface ActionButtonsProps {
  onRandomTheme: () => void;
  currentSeason: SeasonData;
}

export const ActionButtons: React.FC<ActionButtonsProps> = ({ 
  onRandomTheme, 
  currentSeason 
}) => {
  const { showToast } = useToast();
  const handleShare = async () => {
    const url = window.location.href;
    const title = `${currentSeason.name} - 二十四节气主题页面`;
    const text = `来看看这个美丽的${currentSeason.name}主题页面!`;

    if (navigator.share) {
      try {
        await navigator.share({ title, text, url });
        // 分享成功
        showToast('分享成功!', 'success');
      } catch (error: any) {
        // 处理分享取消或其他错误
        if (error.name === 'AbortError') {
          // 用户取消了分享,静默处理
          console.log('用户取消了分享');
        } else {
          // 其他错误,回退到复制链接
          console.error('分享失败,回退到复制链接:', error);
          fallbackToCopy(url);
        }
      }
    } else {
      // 不支持原生分享,直接复制链接
      fallbackToCopy(url);
    }
  };

  const fallbackToCopy = async (url: string) => {
    try {
      await navigator.clipboard.writeText(url);
      showToast('链接已复制到剪贴板!', 'success');
    } catch (error) {
      console.error('复制到剪贴板失败:', error);
      // 最后的回退方案:显示链接让用户手动复制
      showToast(`请手动复制链接:${url}`, 'info');
    }
  };

  return (
    <div className="action-buttons">
      <button 
        className="btn btn-primary" 
        onClick={onRandomTheme}
        title="按空格键也可以随机切换"
      >
        随机切换主题
      </button>
      <button 
        className="btn btn-outline" 
        onClick={handleShare}
      >
        分享主题
      </button>
    </div>
  );
};

src/components/SeasonCard.tsx

/**
 * 节气信息卡片组件
 */

import React, { ReactNode } from 'react';

interface SeasonCardProps {
  title: string;
  content?: string;
  children?: ReactNode;
}

export const SeasonCard: React.FC<SeasonCardProps> = ({ 
  title, 
  content, 
  children 
}) => {
  return (
    <div className="season-card">
      <h3>{title}</h3>
      {content && <p className="card-content">{content}</p>}
      {children}
    </div>
  );
};

src/components/Toast.tsx

/**
 * Toast 提示组件
 * 用于显示操作反馈信息
 */

import React, { useState, useEffect } from 'react';

interface ToastProps {
  message: string;
  type?: 'success' | 'error' | 'info';
  duration?: number;
  onClose?: () => void;
}

export const Toast: React.FC<ToastProps> = ({ 
  message, 
  type = 'info', 
  duration = 3000,
  onClose 
}) => {
  const [isVisible, setIsVisible] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsVisible(false);
      setTimeout(() => onClose?.(), 300); // 等待动画完成
    }, duration);

    return () => clearTimeout(timer);
  }, [duration, onClose]);

  const getToastClass = () => {
    const baseClass = 'toast';
    const typeClass = `toast-${type}`;
    const visibilityClass = isVisible ? 'toast-visible' : 'toast-hidden';
    return `${baseClass} ${typeClass} ${visibilityClass}`;
  };

  return (
    <div className={getToastClass()}>
      <div className="toast-content">
        <span className="toast-message">{message}</span>
        <button 
          className="toast-close" 
          onClick={() => {
            setIsVisible(false);
            setTimeout(() => onClose?.(), 300);
          }}
        >
          ×
        </button>
      </div>
    </div>
  );
};

// Toast 管理器 Hook
export const useToast = () => {
  const [toasts, setToasts] = useState<Array<{
    id: string;
    message: string;
    type: 'success' | 'error' | 'info';
  }>>([]);

  const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type }]);
  };

  const removeToast = (id: string) => {
    setToasts(prev => prev.filter(toast => toast.id !== id));
  };

  const ToastContainer: React.FC = () => (
    <div className="toast-container">
      {toasts.map(toast => (
        <Toast
          key={toast.id}
          message={toast.message}
          type={toast.type}
          onClose={() => removeToast(toast.id)}
        />
      ))}
    </div>
  );

  return { showToast, ToastContainer };
};

src/types/global.d.ts

// 全局类型声明:声明节气数据在 window 上的注入形态
// 目的:避免在读取 window.__SEASON_DATA__ 时出现 any,提供类型提示

import type { SeasonData } from '@/data/seasons';

declare global {
  interface Window {
    __SEASON_DATA__?: SeasonData;
  }
}

export {};

常用命令

  • 安装依赖:
npm install
  • 单入口(SPA)开发(HMR + React Refresh):
npm run dev

访问 http://localhost:3000/

  • 多入口(单个节气)开发:
npm run start lichun

将打开 http://localhost:3000/lichun/(可替换 lichun 为其他节气名)

  • 构建单个节气(生产):
npm run build jingzhe

产物在 dist/jingzhe/

  • 构建所有节气:
npm run build all

产物位于 dist/<season>/

  • 产物分析(示例:多入口):
ANALYZE=true npm run build:multi
  • 代码质量:
npm run lint
npm run lint:fix
npm run type-check

常见问题(FAQ)

  • 访问空白页或 404

    • 单入口模式访问根路径 /,多入口模式访问 /<season>/
    • 开发服务器 historyApiFallback 已启用,确保使用上述命令启动
  • 别名导入报错

    • 确认 tsconfig.jsonbaseUrl/pathswebpackresolve.alias 一致
  • React Fast Refresh 不生效

    • 使用 npm run devnpm run start <season>,确保浏览器允许 HMR
  • Node 版本过低

    • 升级到 Node >= 16