Преглед на файлове

feat: 填字游戏开发

lanxin преди 3 месеца
родител
ревизия
f0c8c2f178
променени са 45 файла, в които са добавени 2263 реда и са изтрити 128 реда
  1. 657 0
      package-lock.json
  2. 7 2
      package.json
  3. 0 10
      public/index.html
  4. 0 38
      src/App.css
  5. 0 9
      src/App.test.tsx
  6. 0 26
      src/App.tsx
  7. 516 0
      src/assets/data/questions.ts
  8. BIN
      src/assets/img/back.png
  9. BIN
      src/assets/img/bg.png
  10. BIN
      src/assets/img/button_bg.png
  11. BIN
      src/assets/img/button_bg2.png
  12. BIN
      src/assets/img/button_bg3.png
  13. BIN
      src/assets/img/false.png
  14. BIN
      src/assets/img/home_title.png
  15. BIN
      src/assets/img/input_bg.png
  16. BIN
      src/assets/img/option_false.png
  17. BIN
      src/assets/img/option_true.png
  18. BIN
      src/assets/img/or.png
  19. BIN
      src/assets/img/question_title.png
  20. BIN
      src/assets/img/rank_bg.png
  21. BIN
      src/assets/img/rank_current.png
  22. BIN
      src/assets/img/rank_item.png
  23. BIN
      src/assets/img/rank_left.png
  24. BIN
      src/assets/img/rank_right.png
  25. BIN
      src/assets/img/rank_title.png
  26. BIN
      src/assets/img/time.png
  27. BIN
      src/assets/img/true.png
  28. 16 13
      src/index.css
  29. 85 8
      src/index.tsx
  30. 0 1
      src/logo.svg
  31. 30 0
      src/page/home/iindex.tsx
  32. 78 0
      src/page/home/index.module.scss
  33. 123 0
      src/page/question/components/index.module.scss
  34. 93 0
      src/page/question/components/index.tsx
  35. 145 0
      src/page/question/index.module.scss
  36. 153 0
      src/page/question/index.tsx
  37. 159 0
      src/page/rank/index.module.scss
  38. 129 0
      src/page/rank/index.tsx
  39. 0 1
      src/react-app-env.d.ts
  40. 0 15
      src/reportWebVitals.ts
  41. 0 5
      src/setupTests.ts
  42. 8 0
      src/type/declaration.d.ts
  43. 36 0
      src/utils/API.ts
  44. 8 0
      src/utils/http.ts
  45. 20 0
      src/utils/storage.ts

+ 657 - 0
package-lock.json

@@ -16,9 +16,13 @@
         "@types/node": "^16.18.126",
         "@types/react": "^19.1.2",
         "@types/react-dom": "^19.1.2",
+        "axios": "^1.9.0",
+        "dayjs": "^1.11.13",
         "react": "^19.1.0",
         "react-dom": "^19.1.0",
+        "react-router-dom": "^7.5.1",
         "react-scripts": "5.0.1",
+        "sass": "^1.87.0",
         "typescript": "^4.9.5",
         "web-vitals": "^2.1.4"
       }
@@ -2837,6 +2841,288 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@parcel/watcher": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz",
+      "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+      "hasInstallScript": true,
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^1.0.3",
+        "is-glob": "^4.0.3",
+        "micromatch": "^4.0.5",
+        "node-addon-api": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.5.1",
+        "@parcel/watcher-darwin-arm64": "2.5.1",
+        "@parcel/watcher-darwin-x64": "2.5.1",
+        "@parcel/watcher-freebsd-x64": "2.5.1",
+        "@parcel/watcher-linux-arm-glibc": "2.5.1",
+        "@parcel/watcher-linux-arm-musl": "2.5.1",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+        "@parcel/watcher-linux-arm64-musl": "2.5.1",
+        "@parcel/watcher-linux-x64-glibc": "2.5.1",
+        "@parcel/watcher-linux-x64-musl": "2.5.1",
+        "@parcel/watcher-win32-arm64": "2.5.1",
+        "@parcel/watcher-win32-ia32": "2.5.1",
+        "@parcel/watcher-win32-x64": "2.5.1"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+      "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+      "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+      "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+      "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+      "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+      "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+      "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+      "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+      "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+      "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+      "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-ia32": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+      "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+      "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
     "node_modules/@pkgjs/parseargs": {
       "version": "0.11.0",
       "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -4569,6 +4855,30 @@
         "node": ">=4"
       }
     },
+    "node_modules/axios": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz",
+      "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/axios/node_modules/form-data": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz",
+      "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/axobject-query": {
       "version": "4.1.0",
       "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -5988,6 +6298,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/dayjs": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+    },
     "node_modules/debug": {
       "version": "4.4.0",
       "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz",
@@ -6111,6 +6426,18 @@
         "npm": "1.2.8000 || >= 1.4.16"
       }
     },
+    "node_modules/detect-libc": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz",
+      "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+      "optional": true,
+      "bin": {
+        "detect-libc": "bin/detect-libc.js"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/detect-newline": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -8510,6 +8837,11 @@
         "url": "https://opencollective.com/immer"
       }
     },
+    "node_modules/immutable": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.1.tgz",
+      "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg=="
+    },
     "node_modules/import-fresh": {
       "version": "3.3.1",
       "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -10711,6 +11043,12 @@
         "tslib": "^2.0.3"
       }
     },
+    "node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "optional": true
+    },
     "node_modules/node-forge": {
       "version": "1.3.1",
       "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz",
@@ -12633,6 +12971,11 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "node_modules/psl": {
       "version": "1.15.0",
       "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz",
@@ -12906,6 +13249,51 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/react-router": {
+      "version": "7.5.1",
+      "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.5.1.tgz",
+      "integrity": "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==",
+      "dependencies": {
+        "cookie": "^1.0.1",
+        "set-cookie-parser": "^2.6.0",
+        "turbo-stream": "2.4.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "7.5.1",
+      "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.5.1.tgz",
+      "integrity": "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==",
+      "dependencies": {
+        "react-router": "7.5.1"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      }
+    },
+    "node_modules/react-router/node_modules/cookie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz",
+      "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/react-scripts": {
       "version": "5.0.1",
       "resolved": "https://registry.npmmirror.com/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -13480,6 +13868,25 @@
       "resolved": "https://registry.npmmirror.com/sanitize.css/-/sanitize.css-13.0.0.tgz",
       "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
     },
+    "node_modules/sass": {
+      "version": "1.87.0",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.87.0.tgz",
+      "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
+      "dependencies": {
+        "chokidar": "^4.0.0",
+        "immutable": "^5.0.2",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
     "node_modules/sass-loader": {
       "version": "12.6.0",
       "resolved": "https://registry.npmmirror.com/sass-loader/-/sass-loader-12.6.0.tgz",
@@ -13517,6 +13924,32 @@
         }
       }
     },
+    "node_modules/sass/node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/sass/node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/sax": {
       "version": "1.2.4",
       "resolved": "https://registry.npmmirror.com/sax/-/sax-1.2.4.tgz",
@@ -13751,6 +14184,11 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/set-cookie-parser": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+      "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
+    },
     "node_modules/set-function-length": {
       "version": "1.2.2",
       "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -15015,6 +15453,11 @@
       "resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
     },
+    "node_modules/turbo-stream": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/turbo-stream/-/turbo-stream-2.4.0.tgz",
+      "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
+    },
     "node_modules/type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
@@ -18069,6 +18512,109 @@
         "fastq": "^1.6.0"
       }
     },
+    "@parcel/watcher": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz",
+      "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+      "optional": true,
+      "requires": {
+        "@parcel/watcher-android-arm64": "2.5.1",
+        "@parcel/watcher-darwin-arm64": "2.5.1",
+        "@parcel/watcher-darwin-x64": "2.5.1",
+        "@parcel/watcher-freebsd-x64": "2.5.1",
+        "@parcel/watcher-linux-arm-glibc": "2.5.1",
+        "@parcel/watcher-linux-arm-musl": "2.5.1",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+        "@parcel/watcher-linux-arm64-musl": "2.5.1",
+        "@parcel/watcher-linux-x64-glibc": "2.5.1",
+        "@parcel/watcher-linux-x64-musl": "2.5.1",
+        "@parcel/watcher-win32-arm64": "2.5.1",
+        "@parcel/watcher-win32-ia32": "2.5.1",
+        "@parcel/watcher-win32-x64": "2.5.1",
+        "detect-libc": "^1.0.3",
+        "is-glob": "^4.0.3",
+        "micromatch": "^4.0.5",
+        "node-addon-api": "^7.0.0"
+      }
+    },
+    "@parcel/watcher-android-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+      "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+      "optional": true
+    },
+    "@parcel/watcher-darwin-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+      "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+      "optional": true
+    },
+    "@parcel/watcher-darwin-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+      "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+      "optional": true
+    },
+    "@parcel/watcher-freebsd-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+      "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+      "optional": true
+    },
+    "@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+      "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+      "optional": true
+    },
+    "@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+      "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+      "optional": true
+    },
+    "@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+      "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+      "optional": true
+    },
+    "@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+      "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+      "optional": true
+    },
+    "@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+      "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+      "optional": true
+    },
+    "@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+      "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+      "optional": true
+    },
+    "@parcel/watcher-win32-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+      "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+      "optional": true
+    },
+    "@parcel/watcher-win32-ia32": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+      "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+      "optional": true
+    },
+    "@parcel/watcher-win32-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+      "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+      "optional": true
+    },
     "@pkgjs/parseargs": {
       "version": "0.11.0",
       "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -19330,6 +19876,29 @@
       "resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.10.3.tgz",
       "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="
     },
+    "axios": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz",
+      "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+      "requires": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      },
+      "dependencies": {
+        "form-data": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz",
+          "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+          "requires": {
+            "asynckit": "^0.4.0",
+            "combined-stream": "^1.0.8",
+            "es-set-tostringtag": "^2.1.0",
+            "mime-types": "^2.1.12"
+          }
+        }
+      }
+    },
     "axobject-query": {
       "version": "4.1.0",
       "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -20340,6 +20909,11 @@
         "is-data-view": "^1.0.1"
       }
     },
+    "dayjs": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+    },
     "debug": {
       "version": "4.4.0",
       "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz",
@@ -20421,6 +20995,12 @@
       "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
       "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
     },
+    "detect-libc": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz",
+      "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+      "optional": true
+    },
     "detect-newline": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -22141,6 +22721,11 @@
       "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
       "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
     },
+    "immutable": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.1.tgz",
+      "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg=="
+    },
     "import-fresh": {
       "version": "3.3.1",
       "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -23735,6 +24320,12 @@
         "tslib": "^2.0.3"
       }
     },
+    "node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "optional": true
+    },
     "node-forge": {
       "version": "1.3.1",
       "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz",
@@ -24928,6 +25519,11 @@
         }
       }
     },
+    "proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "psl": {
       "version": "1.15.0",
       "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz",
@@ -25125,6 +25721,31 @@
       "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.11.0.tgz",
       "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
     },
+    "react-router": {
+      "version": "7.5.1",
+      "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.5.1.tgz",
+      "integrity": "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==",
+      "requires": {
+        "cookie": "^1.0.1",
+        "set-cookie-parser": "^2.6.0",
+        "turbo-stream": "2.4.0"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz",
+          "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="
+        }
+      }
+    },
+    "react-router-dom": {
+      "version": "7.5.1",
+      "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.5.1.tgz",
+      "integrity": "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==",
+      "requires": {
+        "react-router": "7.5.1"
+      }
+    },
     "react-scripts": {
       "version": "5.0.1",
       "resolved": "https://registry.npmmirror.com/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -25524,6 +26145,32 @@
       "resolved": "https://registry.npmmirror.com/sanitize.css/-/sanitize.css-13.0.0.tgz",
       "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
     },
+    "sass": {
+      "version": "1.87.0",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.87.0.tgz",
+      "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
+      "requires": {
+        "@parcel/watcher": "^2.4.1",
+        "chokidar": "^4.0.0",
+        "immutable": "^5.0.2",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "dependencies": {
+        "chokidar": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
+          "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+          "requires": {
+            "readdirp": "^4.0.1"
+          }
+        },
+        "readdirp": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
+          "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
+        }
+      }
+    },
     "sass-loader": {
       "version": "12.6.0",
       "resolved": "https://registry.npmmirror.com/sass-loader/-/sass-loader-12.6.0.tgz",
@@ -25728,6 +26375,11 @@
         "send": "0.19.0"
       }
     },
+    "set-cookie-parser": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+      "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
+    },
     "set-function-length": {
       "version": "1.2.2",
       "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -26673,6 +27325,11 @@
         }
       }
     },
+    "turbo-stream": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/turbo-stream/-/turbo-stream-2.4.0.tgz",
+      "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
+    },
     "type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",

+ 7 - 2
package.json

@@ -11,14 +11,18 @@
     "@types/node": "^16.18.126",
     "@types/react": "^19.1.2",
     "@types/react-dom": "^19.1.2",
+    "axios": "^1.9.0",
+    "dayjs": "^1.11.13",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
+    "react-router-dom": "^7.5.1",
     "react-scripts": "5.0.1",
+    "sass": "^1.87.0",
     "typescript": "^4.9.5",
     "web-vitals": "^2.1.4"
   },
   "scripts": {
-    "start": "react-scripts start",
+    "dev": "react-scripts start",
     "build": "react-scripts build",
     "test": "react-scripts test",
     "eject": "react-scripts eject"
@@ -40,5 +44,6 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
-  }
+  },
+  "homepage": "https://houseoss.4dkankan.com/project/QingdaoChallenge/"
 }

+ 0 - 10
public/index.html

@@ -29,15 +29,5 @@
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <div id="root"></div>
-    <!--
-      This HTML file is a template.
-      If you open it directly in the browser, you will see an empty page.
-
-      You can add webfonts, meta tags, or analytics to this file.
-      The build step will place the bundled scripts into the <body> tag.
-
-      To begin the development, run `npm start` or `yarn start`.
-      To create a production bundle, use `npm run build` or `yarn build`.
-    -->
   </body>
 </html>

+ 0 - 38
src/App.css

@@ -1,38 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}

+ 0 - 9
src/App.test.tsx

@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
-  render(<App />);
-  const linkElement = screen.getByText(/learn react/i);
-  expect(linkElement).toBeInTheDocument();
-});

+ 0 - 26
src/App.tsx

@@ -1,26 +0,0 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
-
-function App() {
-  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.tsx</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
-  );
-}
-
-export default App;

+ 516 - 0
src/assets/data/questions.ts

@@ -0,0 +1,516 @@
+const questions = [
+  [
+    {
+      id: 1,
+      qid: 1,
+      title:
+        "“()交错天文也,文明以止人文也。关乎天文以察时变,关乎人文以化成天下。”——《周易贲卦彖辞》",
+      options: [
+        {
+          id: 1,
+          title: "刚柔",
+        },
+        {
+          id: 2,
+          title: "强弱",
+        },
+        {
+          id: 3,
+          title: "动静",
+        },
+        {
+          id: 4,
+          title: "阴阳",
+        },
+      ],
+    },
+    {
+      id: 1,
+      qid: 2,
+      title:
+        "“刚柔交错天文也,()以止人文也。关乎天文以察时变,关乎人文以化成天下。”——《周易贲卦彖辞》",
+      options: [
+        {
+          id: 1,
+          title: "文明",
+        },
+        {
+          id: 2,
+          title: "文采",
+        },
+        {
+          id: 3,
+          title: "文雅",
+        },
+        {
+          id: 4,
+          title: "文化",
+        },
+      ],
+    },
+  ],
+  [
+    {
+      id: 2,
+      qid: 1,
+      title: "“濬哲(),温恭允塞。”——《尚书舜典》",
+      options: [
+        {
+          id: 1,
+          title: "文明",
+        },
+        {
+          id: 2,
+          title: "明德",
+        },
+        {
+          id: 3,
+          title: "礼乐",
+        },
+        {
+          id: 4,
+          title: "圣贤",
+        },
+      ],
+    },
+    {
+      id: 2,
+      qid: 2,
+      title: "“濬哲文明,温恭()。”——《尚书舜典》",
+      options: [
+        {
+          id: 1,
+          title: "允塞",
+        },
+        {
+          id: 2,
+          title: "允德",
+        },
+        {
+          id: 3,
+          title: "允恭",
+        },
+        {
+          id: 4,
+          title: "允诚",
+        },
+      ],
+    },
+  ],
+  [
+    {
+      id: 3,
+      qid: 1,
+      title: "“以()邦国,以谐万民。” ——《周礼天官大宰》",
+      options: [
+        {
+          id: 1,
+          title: "和",
+        },
+        {
+          id: 2,
+          title: "平",
+        },
+        {
+          id: 3,
+          title: "安",
+        },
+        {
+          id: 4,
+          title: "治",
+        },
+      ],
+    },
+    {
+      id: 3,
+      qid: 2,
+      title: "“以和邦(),以谐万民。” ——《周礼天官大宰》",
+      options: [
+        {
+          id: 1,
+          title: "国",
+        },
+        {
+          id: 2,
+          title: "家",
+        },
+        {
+          id: 3,
+          title: "城",
+        },
+        {
+          id: 4,
+          title: "社",
+        },
+      ],
+    },
+    {
+      id: 3,
+      qid: 3,
+      title: "“以和邦国,以()万民。” ——《周礼天官大宰》",
+      options: [
+        {
+          id: 1,
+          title: "谐",
+        },
+        {
+          id: 2,
+          title: "协",
+        },
+        {
+          id: 3,
+          title: "调",
+        },
+        {
+          id: 4,
+          title: "睦",
+        },
+      ],
+    },
+    {
+      id: 3,
+      qid: 4,
+      title: "“以和邦国,以谐万()。” ——《周礼天官大宰》",
+      options: [
+        {
+          id: 1,
+          title: "民",
+        },
+        {
+          id: 2,
+          title: "人",
+        },
+        {
+          id: 3,
+          title: "众",
+        },
+        {
+          id: 4,
+          title: "生",
+        },
+      ],
+    },
+  ],
+  [
+    {
+      id: 4,
+      qid: 1,
+      title: "“如()之和,无所不谐。”——《左传襄公十一年》",
+      options: [
+        {
+          id: 1,
+          title: "乐",
+        },
+        {
+          id: 2,
+          title: "礼",
+        },
+        {
+          id: 3,
+          title: "诗",
+        },
+        {
+          id: 4,
+          title: "书",
+        },
+      ],
+    },
+    {
+      id: 4,
+      qid: 2,
+      title: "“如乐之(),无所不谐。”——《左传襄公十一年》",
+      options: [
+        {
+          id: 1,
+          title: "和",
+        },
+        {
+          id: 2,
+          title: "同",
+        },
+        {
+          id: 3,
+          title: "融",
+        },
+        {
+          id: 4,
+          title: "通",
+        },
+      ],
+    },
+    {
+      id: 4,
+      qid: 3,
+      title: "“如乐之和,无所不()。”——《左传襄公十一年》",
+      options: [
+        {
+          id: 1,
+          title: "谐",
+        },
+        {
+          id: 2,
+          title: "调",
+        },
+        {
+          id: 3,
+          title: "协",
+        },
+        {
+          id: 4,
+          title: "顺",
+        },
+      ],
+    },
+  ],
+  [
+    {
+      id: 5,
+      qid: 1,
+      title: "“()贵贱皆从法,此谓为大治。”——《春秋管仲》",
+      options: [
+        { id: 1, title: "君臣上下" },
+        { id: 2, title: "父子长幼" },
+        { id: 3, title: "士农工商" },
+        { id: 4, title: "礼乐刑政" },
+      ],
+    },
+    {
+      id: 5,
+      qid: 2,
+      title: "“君臣上下贵贱皆从(),此谓为大治。”——《春秋管仲》",
+      options: [
+        { id: 1, title: "法" },
+        { id: 2, title: "礼" },
+        { id: 3, title: "德" },
+        { id: 4, title: "律" },
+      ],
+    },
+    {
+      id: 5,
+      qid: 3,
+      title: "“君臣上下贵贱皆从法,此谓为()。”——《春秋管仲》",
+      options: [
+        { id: 1, title: "大治" },
+        { id: 2, title: "大同" },
+        { id: 3, title: "太平" },
+        { id: 4, title: "昌盛" },
+      ],
+    },
+  ],
+  [
+    {
+      id: 6,
+      qid: 1,
+      title: "“()之行也,天下为公。 ”——《礼记礼运》",
+      options: [
+        { id: 1, title: "大道" },
+        { id: 2, title: "天道" },
+        { id: 3, title: "王道" },
+        { id: 4, title: "圣道" },
+      ],
+    },
+    {
+      id: 6,
+      qid: 2,
+      title: "“大道之行也,()为公。 ”——《礼记礼运》",
+      options: [
+        { id: 1, title: "天下" },
+        { id: 2, title: "四海" },
+        { id: 3, title: "九州" },
+        { id: 4, title: "万方" },
+      ],
+    },
+    {
+      id: 6,
+      qid: 3,
+      title: "“大道之行也,天下为()。 ”——《礼记礼运》",
+      options: [
+        { id: 1, title: "公" },
+        { id: 2, title: "正" },
+        { id: 3, title: "平" },
+        { id: 4, title: "明" },
+      ],
+    },
+  ],
+  [
+    {
+      id: 7,
+      qid: 1,
+      title: "“一年视()辨志,三年视敬业乐群。”——《礼记学记》",
+      options: [
+        { id: 1, title: "离经" },
+        { id: 2, title: "通经" },
+        { id: 3, title: "读经" },
+        { id: 4, title: "习经" },
+      ],
+    },
+    {
+      id: 7,
+      qid: 2,
+      title: "“一年视离经辨志,()视敬业乐群。”——《礼记学记》",
+      options: [
+        { id: 1, title: "三年" },
+        { id: 2, title: "五载" },
+        { id: 3, title: "七岁" },
+        { id: 4, title: "十旬" },
+      ],
+    },
+    {
+      id: 7,
+      qid: 3,
+      title: "“一年视离经辨志,三年视()乐群。”——《礼记学记》",
+      options: [
+        { id: 1, title: "敬业" },
+        { id: 2, title: "勤业" },
+        { id: 3, title: "精业" },
+        { id: 4, title: "守业" },
+      ],
+    },
+  ],
+  [
+    {
+      id: 8,
+      qid: 1,
+      title: "“(),国之宝也,民之所庇也。”——《左传》",
+      options: [
+        { id: 1, title: "信" },
+        { id: 2, title: "义" },
+        { id: 3, title: "忠" },
+        { id: 4, title: "仁" },
+      ],
+    },
+    {
+      id: 8,
+      qid: 2,
+      title: "“信,()之宝也,民之所庇也。”——《左传》",
+      options: [
+        { id: 1, title: "国" },
+        { id: 2, title: "邦" },
+        { id: 3, title: "社" },
+        { id: 4, title: "朝" },
+      ],
+    },
+    {
+      id: 8,
+      qid: 3,
+      title: "“信,国之宝也,()之所庇也。”——《左传》",
+      options: [
+        { id: 1, title: "民" },
+        { id: 2, title: "人" },
+        { id: 3, title: "众" },
+        { id: 4, title: "庶" },
+      ],
+    },
+  ],
+  [
+    {
+      id: 9,
+      qid: 1,
+      title:
+        "“子曰:()之道,或出或处,或默或语。二人同心,其利断金。同心之言,其臭如兰。”——《周易系辞上传》",
+      options: [
+        { id: 1, title: "君子" },
+        { id: 2, title: "圣人" },
+        { id: 3, title: "贤者" },
+        { id: 4, title: "士人" },
+      ],
+    },
+    {
+      id: 9,
+      qid: 2,
+      title:
+        "“子曰:君子之道,或出或处,或默或语。二人(),其利断金。同心之言,其臭如兰。”——《周易系辞上传》",
+      options: [
+        { id: 1, title: "同心" },
+        { id: 2, title: "齐心" },
+        { id: 3, title: "协心" },
+        { id: 4, title: "一意" },
+      ],
+    },
+    {
+      id: 9,
+      qid: 3,
+      title:
+        "“子曰:君子之道,或出或处,或默或语。二人同心,其利断()。同心之言,其臭如兰。”——《周易系辞上传》",
+      options: [
+        { id: 1, title: "金" },
+        { id: 2, title: "铁" },
+        { id: 3, title: "玉" },
+        { id: 4, title: "石" },
+      ],
+    },
+  ],
+  [
+    {
+      id: 10,
+      qid: 1,
+      title: "“()惟邦本,本固邦宁。”——《尚书五子之歌》",
+      options: [
+        { id: 1, title: "民" },
+        { id: 2, title: "人" },
+        { id: 3, title: "众" },
+        { id: 4, title: "庶" },
+      ],
+    },
+    {
+      id: 10,
+      qid: 2,
+      title: "“民惟邦本,()固邦宁。”——《尚书五子之歌》",
+      options: [
+        { id: 1, title: "本" },
+        { id: 2, title: "根" },
+        { id: 3, title: "基" },
+        { id: 4, title: "源" },
+      ],
+    },
+    {
+      id: 10,
+      qid: 3,
+      title: "“民惟邦本,本固邦()。”——《尚书五子之歌》",
+      options: [
+        { id: 1, title: "宁" },
+        { id: 2, title: "安" },
+        { id: 3, title: "定" },
+        { id: 4, title: "泰" },
+      ],
+    },
+  ],
+];
+
+// 随机数组
+function shuffle(array: any[]) {
+  let m = array.length;
+  while (m) {
+    const i = Math.floor(Math.random() * m--);
+    [array[m], array[i]] = [array[i], array[m]];
+  }
+  return array;
+}
+
+// 生成3个选项
+function generateThreeOptions(options: { id: number; title: string }[]) {
+  // 错误选项的索引
+  const wrongIndices = [1, 2, 3];
+
+  // 随机选取2个错误选项
+  const selectedWrong: number[] = shuffle(wrongIndices).slice(0, 2);
+
+  // 合并正确项与错误项,并打乱顺序
+  const combined = [options[0], ...selectedWrong.map((i) => options[i])];
+  return shuffle(combined);
+}
+
+// 每个题目有不同问法(即qid不同,id相同)。id为1的是正确答案。
+// 题型:随机每个id的题目,选第一个。几个id几个题目
+// 选项:先随机从2,3,4出两个,再和选项1随机组成最终选项
+// 题型:随机这些题目
+
+const randomQuestions = questions.map((questionArray) => {
+  const question = shuffle(questionArray)[0];
+  return {
+    ...question,
+    options: generateThreeOptions(question.options),
+  };
+});
+
+export default shuffle(randomQuestions);

BIN
src/assets/img/back.png


BIN
src/assets/img/bg.png


BIN
src/assets/img/button_bg.png


BIN
src/assets/img/button_bg2.png


BIN
src/assets/img/button_bg3.png


BIN
src/assets/img/false.png


BIN
src/assets/img/home_title.png


BIN
src/assets/img/input_bg.png


BIN
src/assets/img/option_false.png


BIN
src/assets/img/option_true.png


BIN
src/assets/img/or.png


BIN
src/assets/img/question_title.png


BIN
src/assets/img/rank_bg.png


BIN
src/assets/img/rank_current.png


BIN
src/assets/img/rank_item.png


BIN
src/assets/img/rank_left.png


BIN
src/assets/img/rank_right.png


BIN
src/assets/img/rank_title.png


BIN
src/assets/img/time.png


BIN
src/assets/img/true.png


+ 16 - 13
src/index.css

@@ -1,13 +1,16 @@
-body {
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
-    sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}
+#root {
+  /* width: 100%; */
+  /* height: 100%; */
+  background: url('./assets/img/bg.png') no-repeat center center;
+  background-size: 100% 100%;
+  position: relative;
+}
+body {
+  /* width: 100%;
+  height: 100%; */
+  overflow: hidden;
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: transparent;
+}

+ 85 - 8
src/index.tsx

@@ -1,19 +1,96 @@
-import React from 'react';
+import React, { useRef, useCallback, useEffect } from 'react';
 import ReactDOM from 'react-dom/client';
 import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
-
+import { HashRouter as Router, Routes, Route } from 'react-router-dom';
+import Home from './page/home/iindex';
+import QuestionList from './page/question/index';
+import Rank from './page/rank/index';
 const root = ReactDOM.createRoot(
   document.getElementById('root') as HTMLElement
 );
+
+let tempW = document.documentElement.clientWidth
+let tempH = document.documentElement.clientHeight
+
+let tempMax = tempW >= tempH ? tempW : tempH
+let tempMin = tempW >= tempH ? tempH : tempW
+
+const pageBi = Math.round(Number((tempMax / tempMin).toFixed(2)))
+
+const App = () => {
+  const rootRef = useRef<any>(null)
+  const planSize = {
+    width: 1920,
+    height: Math.round(Number((1920 / pageBi).toFixed(0)))
+  }
+
+  const pageFullChangeFu = useCallback(() => {
+    let width = document.documentElement.clientWidth
+    let height = document.documentElement.clientHeight
+
+
+    if (width >= height) {
+      if (tempMax - width > 100) return
+
+      const sizeW = width / planSize.width
+      let sizeH = height / planSize.height
+
+      let moveX = (planSize.width - width) / 2
+      let moveY = (planSize.height - height) / 2
+
+      if (width >= planSize.width) moveX = 0
+      rootRef.current.style.left = '0'
+      rootRef.current.style.transform = `translate(${-moveX}px,${-moveY}px) scale(${sizeW},${sizeH}) rotate(0deg)`
+      rootRef.current.style.transformOrigin = 'center'
+    } else {
+      if (tempMax - height > 100) return
+
+      // 竖屏
+      const temp = width
+      width = height
+      height = temp
+
+      const sizeW = width / planSize.width
+      let sizeH = height / planSize.height
+
+      rootRef.current.style.left = '100%'
+      rootRef.current.style.transform = `rotate(90deg) scale(${sizeW},${sizeH})`
+
+      rootRef.current.style.transformOrigin = 'left top'
+    }
+
+    // 横竖屏变化的时候 刷新页面
+
+    // if (window.isHH !== isHHTemp) {
+    //   window.location.reload()
+    // }
+
+  }, [planSize.height, planSize.width])
+
+  useEffect(() => {
+    rootRef.current = document.querySelector('#root')
+    rootRef.current.style.width = planSize.width + 'px'
+    rootRef.current.style.height = planSize.height + 'px'
+
+    pageFullChangeFu()
+    window.addEventListener('resize', pageFullChangeFu, false)
+  }, [pageFullChangeFu, planSize.height, planSize.width])
+
+  return (
+    <Router>
+      <Routes>
+        <Route path="/" element={<Home />} />
+        <Route path="/question" element={<QuestionList />} />
+        <Route path="/rank" element={<Rank />} />
+      </Routes>
+    </Router>
+  );
+};
+
+
 root.render(
   <React.StrictMode>
     <App />
   </React.StrictMode>
 );
 
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();

Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
src/logo.svg


+ 30 - 0
src/page/home/iindex.tsx

@@ -0,0 +1,30 @@
+import React from "react";
+import styles from "./index.module.scss";
+import { useNavigate } from "react-router-dom";
+
+const Home = () => {
+  const navigate = useNavigate();
+  const handleBack = () => {
+    window.parent.postMessage('back', '*');
+    console.log('传递了back');
+  }
+  return (
+    <div className={styles.home}>
+      <div className="back" onClick={handleBack}><img src={require('../../assets/img/back.png')} alt="" /></div>
+      <div className="title"><img src={require('../../assets/img/home_title.png')} alt="" /></div>
+      <div className="content">
+        游戏规则:答对更多题目耗时更少的前五名 将在展览结束后赠送文创书签纪念品。
+      </div>
+      <div className="btn-group">
+        <div className="btn-start" onClick={() => {
+          navigate('/question');
+        }}>开始游戏</div>
+        <div className="btn-rank" onClick={() => {
+          navigate('/rank');
+        }}>查看排行榜</div>
+      </div>
+    </div>
+  );
+};
+
+export default Home;

+ 78 - 0
src/page/home/index.module.scss

@@ -0,0 +1,78 @@
+.home {
+  width: 100%;
+  height: 100%;
+  :global {
+    .back {
+      width: 75px;
+      height: 60px;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      cursor: pointer;
+      transform: translate(-900px, -410px);
+      img {
+        object-fit: contain;
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .title {
+      width: 750px;
+      height: 200px;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -350px);
+      img {
+        object-fit: contain;
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .content {
+      font-family: '宋体';
+      width: 600px;
+      height: 95px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 28px;
+      line-height: 40px;
+      color: #fff;
+      text-align: center;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -120px);
+    }
+    .btn-group {
+      width: 270px;
+      height: 180px;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, 160px);
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      justify-content: center;
+      gap: 35px;
+      .btn-start, .btn-rank {
+        font-family: 'STKaiti-Regular';
+        width: 100%;
+        height: 80px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background: url('../../assets/img/button_bg.png') no-repeat center center;
+        background-size: 100% 100%;
+        font-size: 38px;
+        color: #fff;
+        font-weight: bold;
+        text-align: center;
+        line-height: 180px;
+        cursor: pointer;
+      }
+    }
+  }
+}

+ 123 - 0
src/page/question/components/index.module.scss

@@ -0,0 +1,123 @@
+.infomation {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center; 
+  gap: 20px;
+  :global {
+    .back {
+      width: 75px;
+      height: 60px;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-900px, -410px);
+      cursor: pointer;
+      img {
+        object-fit: contain;
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .title {
+      width: 100%;
+      height: 70px;
+      font-size: 42px;
+      line-height: 70px;
+      text-align: center;
+      color: rgba(213, 164, 91, 1);
+    }
+    .content {
+      width: 100%;
+      height: 60px;
+      font-size: 30px;
+      line-height: 60px;
+      text-align: center;
+      color: #fff;
+      >span {
+        color: rgba(213, 164, 91, 1);
+      }
+    }
+    .from {
+      width: 100%;
+      height: 55px;
+      padding: 20px 0;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      gap: 20px;
+      >input {
+        width: 500px;
+        height: 100%;
+        background: url(../../../assets/img/input_bg.png) no-repeat center center;
+        background-size: 100% 100%;
+        color: rgba(255, 255, 255, .8);
+        border: none;
+        border-radius: 10px;
+        padding: 0 20px;
+        font-size: 19px;
+        &::placeholder {
+          color: rgba(255, 255, 255, .5);
+          text-align: center;
+        }
+        &:focus {
+          outline: none;
+        }
+      }
+    }
+    .content2 {
+      width: 480px;
+      height: 180px;
+      color: rgba(255, 255, 255, .8);
+      font-size: 24px;
+      line-height: 40px;
+      text-align: center;
+    }
+    .tips {
+      width: 580px;
+      height: 40px;
+      font-size: 20px;
+      line-height: 40px;
+      text-align: center;
+      color: rgba(255, 249, 249, .8);
+      margin-top: 20px;
+      background: linear-gradient(90deg, rgba(89, 110, 141, 0.35) 0%, #596E8D 49%, rgba(89, 110, 141, 0.35) 100%);
+      cursor: default;
+    }
+    .btn-group {
+      width: 100%;
+      height: 100px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      gap: 20px;
+      font-size: 28px;
+      color: rgba(255, 240, 220, 1);
+      text-align: center;
+      line-height: 70px;
+      cursor: pointer;
+      .item {
+        width: 220px;
+        height: 70px;
+        background: url(../../../assets/img/button_bg.png) no-repeat center center;
+        background-size: 100% 100%;
+      }
+      .backHome {
+        width: 220px;
+        height: 70px;
+        color: rgba(255, 240, 220, .5);
+        background: url(../../../assets/img/button_bg2.png) no-repeat center center;
+        background-size: 100% 100%;
+      }
+      
+    }
+    
+  }
+}
+

+ 93 - 0
src/page/question/components/index.tsx

@@ -0,0 +1,93 @@
+import React, { useState, useEffect } from "react";
+import styles from "./index.module.scss";
+import { useNavigate } from "react-router-dom";
+import { getRankList, RankItem, RankDescItem, saveScore } from "../../../utils/API";
+interface ResultProps {
+  timeLeft: number;
+  rightCount: number;
+}
+
+// 对数据进行排序
+const getRankDesc = (rankList: RankItem[], rightCount: number, exhauseTime: number) => {
+  const rankArr: RankDescItem[] = rankList.map(item => {
+    return { score: item.score, time: item.time, rank: -1, isCurrentUser: false }
+  })
+  rankArr.push({ score: rightCount, time: exhauseTime, rank: -1, isCurrentUser: true })
+  rankArr.sort((a, b) => {
+    if (a.score !== b.score) {
+      // 得分不同时,得分高的排在前面
+      return b.score - a.score;
+    } else {
+      // 得分相同时,花费时间少的排在前面
+      return a.time - b.time;
+    }
+  });
+  // 为排序后的数据添加排名信息
+  rankArr.forEach((item, index) => {
+    item.rank = index + 1;
+  });
+
+  return rankArr
+}
+
+const Result = ({ timeLeft, rightCount }: ResultProps) => {
+  const navigate = useNavigate()
+  const [nickname, setNickname] = useState("")
+  const [phone, setPhone] = useState("")
+  const [isShowTips, setIsShowTips] = useState(true)
+  const exhausetime = 180 - timeLeft
+  const [rankDesc, setRankDesc] = useState<RankDescItem[]>([])
+
+  const handleInputChange = () => {
+    if ((nickname.length > 5 || nickname.length === 0) || (phone.length > 20 || phone.length === 0)) {
+      setIsShowTips(true)
+    } else {
+      setIsShowTips(false)
+    }
+  }
+
+  const handleRank = () => {
+    if (!isShowTips) {
+      saveScore({
+        name: nickname,
+        phone: phone,
+        score: rightCount * 10,
+        time: exhausetime
+      }).then(() => {
+        navigate("/rank")
+      })
+    }
+  }
+
+  useEffect(() => {
+    getRankList().then(res => {
+      const rankDescResult = getRankDesc(res.data.data, rightCount, 180 - timeLeft)
+      setRankDesc(rankDescResult)
+    })
+  }, [])
+  return (
+    <div className={styles.infomation}>
+      <div className="title">恭喜您!
+      </div>
+      <div className="content">
+        恭喜您! 共答对 <span>{rightCount}</span> 道题,耗时 <span>{exhausetime}</span> 秒。当前排名第<span>{rankDesc.find(item => item.isCurrentUser)?.rank}</span>
+      </div>
+      <div className="from">
+        <input value={nickname} onChange={(e) => setNickname(e.target.value)} onBlur={handleInputChange} type="text" placeholder="请输入昵称,不超过5个字" />
+        <input value={phone} onChange={(e) => setPhone(e.target.value)} onBlur={handleInputChange} type="text" placeholder="请输入联系方式,不超过20个字" />
+      </div>
+      <div className="content2">
+        游戏规则:答对更多题目耗时更少的前五名 将在展览结束后赠送文创书签纪念品。
+      </div>
+      <div className="tips" style={{ opacity: isShowTips ? 1 : 0 }}>您未填写昵称和联系方式,无法参与此次活动</div>
+      <div className="btn-group">
+        <div className={"backHome item"} onClick={() => navigate("/")}>返回首页</div>
+        <div className="item" onClick={handleRank}>查看排行榜</div>
+      </div>
+      <div className="back"><img src={require("../../../assets/img/back.png")} alt="" /></div>
+
+    </div >
+  );
+};
+
+export default Result;

+ 145 - 0
src/page/question/index.module.scss

@@ -0,0 +1,145 @@
+.question {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  :global {
+    .banner {
+      width: 100%;
+      height: 100px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 15px;
+      cursor: pointer;
+      img {
+        object-fit: contain;
+        width: 40px;
+        height: 40px;
+      }
+    }
+    .title {
+      width: 336px;
+      height: 40px;
+      font-size: 36px;
+      line-height: 40px;
+      text-align: center;
+      color: rgba(213, 164, 91, 1);
+      background: url("../../assets/img/question_title.png") no-repeat center center;
+      margin-top: 50px;
+    }
+
+    .questions {
+      width: 100%;
+      height: 400px;
+      font-size: 36px;
+      color: #fff;
+      font-weight: 300;
+      margin-top: 40px;
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      justify-content: flex-start;
+      .content {
+        width: 1582px;
+        font-size: 36px;
+        line-height: 60px;
+      }
+      .from {
+        width: 1582px;
+        font-size: 36px;
+        line-height: 60px;
+        text-align: right;
+      }
+    }
+
+    .btn-group {
+      width: 100%;
+      height: 100px;
+      color: rgba(51, 51, 51, 1);
+      font-size: 36px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 20px;
+      margin: 20px 0;
+      &> div {
+        width: 355px;
+        height: 103px;
+        font-size: 36px;
+        line-height: 95px;
+        text-align: center;
+        background: url("../../assets/img/button_bg3.png") no-repeat center center;
+        background-size: 90% 90%;
+        cursor: pointer;
+        //去除点击聚焦
+        outline: none;
+        &:focus{
+          outline: none;
+        }
+        &> img {
+          margin-left: 10px;
+          width: 30px;
+          height: 30px;
+          object-fit: contain;
+        }
+      }
+      
+
+    }
+
+    .tips {
+      width: 365px;
+      height: 47px;
+      font-size: 22px;
+      line-height: 47px;
+      text-align: center;
+      color: rgba(255, 255, 255, .8);
+      margin-top: 20px;
+      background: linear-gradient(90deg, rgba(89, 110, 141, 0.35) 0%, #596E8D 49%, rgba(89, 110, 141, 0.35) 100%);
+      cursor: default;
+    }
+    .back {
+      width: 75px;
+      height: 60px;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-900px, -410px);
+      cursor: pointer;
+      img {
+        object-fit: contain;
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .time {
+      width: 200px;
+      height: 80px;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(730px, -410px);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 10px;
+      img {
+        object-fit: contain;
+        width: 60px;
+        height: 80px;
+      }
+      .time-text {
+        width: 120px;
+        height: 80px;
+        font-size: 44px;
+        color: #fff;
+        font-weight: bold;
+        display: flex;
+        align-items: flex-end;
+      }
+    }
+  }
+}

+ 153 - 0
src/page/question/index.tsx

@@ -0,0 +1,153 @@
+import React, { useState, useEffect } from "react";
+import styles from "./index.module.scss";
+import randomQuestions from "../../assets/data/questions";
+import { getSessionStorage, setSessionStorage, clearSessionStorage } from "../../utils/storage";
+import { useNavigate } from "react-router-dom";
+import dayjs from "dayjs";
+import Result from "./components/index";
+
+interface QuestionItem {
+  id: number;
+  title: string;
+  isClicked?: boolean;
+  options: { id: number; title: string }[];
+}
+
+interface QuestionProps {
+  item: QuestionItem;
+  setItem: (item: QuestionItem) => void;
+  currentIndex: number;
+  setCurrentIndex: (index: number) => void;
+  setIsFinish: (isFinish: boolean) => void;
+  setRightCount: (rightCount: number) => void;
+}
+
+const Question = ({ item, setItem, currentIndex, setCurrentIndex, setIsFinish, setRightCount }: QuestionProps) => {
+  const sentence = item.title.split("——")[0];
+  const from = item.title.split("——")[1];
+  const [questionStatus, setQuestionStatus] = useState(["or", "or", "or", "or", "or", "or", "or", "or", "or", "or"]);
+  let isClicked: boolean = getSessionStorage(`question${currentIndex}`)?.isClicked ?? false;
+
+  useEffect(() => {
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    isClicked = getSessionStorage(`question${currentIndex}`)?.isClicked ?? false;
+  }, [currentIndex])
+
+  const handleBtnClick = (id: number) => {
+    if (isClicked) return;
+    isClicked = true
+    const currentStatus = questionStatus.map((status, index) => index === currentIndex ? id === 1 ? "true" : "false" : status)
+    setQuestionStatus(currentStatus)
+    setItem({ ...item, isClicked: true })
+    setSessionStorage(`question${currentIndex}`, { ...item, isClicked: true })
+    setTimeout(() => {
+      if (currentStatus.every(status => status === "true" || status === "false")) {
+        setRightCount(currentStatus.filter(status => status === "true").length)
+
+        setIsFinish(true)
+      } else {
+        if (currentIndex === 9) {
+          const index = currentStatus.findIndex((status, index) => { return status === "or" })
+          handleBannerClick(index)
+        } else {
+          handleBannerClick(currentIndex + 1)
+        }
+      }
+    }, 1000)
+  }
+
+  const handleBannerClick = (newIndex: number) => {
+    setItem(getSessionStorage(`question${newIndex}`) ?? randomQuestions[newIndex]);
+    setCurrentIndex(newIndex)
+    setSessionStorage(`question${currentIndex}`, { ...item, isClicked: isClicked })
+  }
+
+  const chineseNumbers = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"]
+  return (
+    <>
+      <div className="banner">
+        {questionStatus.map((status, index) => (
+          <img key={index} src={require(`../../assets/img/${status}.png`)} onClick={() => handleBannerClick(index)} alt="banner" />
+        ))}
+      </div>
+      <div className="title">题目{chineseNumbers[currentIndex]}</div>
+      <div className="questions">
+        <div className="content">
+          {sentence}
+        </div>
+        <div className="from"> ——{from}</div>
+      </div>
+      <div className="btn-group" >
+        <div className="option1" onClick={() => handleBtnClick(item.options[0].id)}>
+          <span>{item.options[0].title}</span>
+          {item.isClicked && <img src={require(`../../assets/img/option_${item.options[0].id === 1 ? "true" : "false"}.png`)} alt="" />}
+        </div>
+        <div className="option2" onClick={() => handleBtnClick(item.options[1].id)} >
+          <span>{item.options[1].title}</span>
+          {item.isClicked && <img src={require(`../../assets/img/option_${item.options[1].id === 1 ? "true" : "false"}.png`)} alt="" />}
+        </div>
+        <div className="option3" onClick={() => handleBtnClick(item.options[2].id)}>
+          <span>{item.options[2].title}</span>
+          {item.isClicked && <img src={require(`../../assets/img/option_${item.options[2].id === 1 ? "true" : "false"}.png`)} alt="" />}
+        </div>
+      </div>
+    </>
+  );
+};
+
+const QuestionList = () => {
+  const [item, setItem] = useState<QuestionItem>(randomQuestions[0]);
+  const [index, setIndex] = useState(0);
+  const [timeLeft, setTimeLeft] = useState(180);
+  const [isFinish, setIsFinish] = useState(false)
+  const [rightCount, setRightCount] = useState(0)
+
+  const navigate = useNavigate()
+
+  useEffect(() => {
+    clearSessionStorage()
+    const timer = setInterval(() => {
+      setTimeLeft((prevTime) => {
+        if (prevTime <= 0) {
+          clearInterval(timer);
+          setIsFinish(true)
+          return 0;
+        }
+        return prevTime - 1;
+      });
+    }, 1000);
+
+    if (isFinish) {
+      clearInterval(timer);
+    }
+    return () => clearInterval(timer);
+  }, [isFinish])
+
+
+  return (
+    isFinish ? (
+      <Result timeLeft={timeLeft} rightCount={rightCount} />
+    ) : (
+      <div className={styles.question}>
+        <Question
+          item={item}
+          setItem={setItem}
+          currentIndex={index}
+          setCurrentIndex={setIndex}
+          setIsFinish={setIsFinish}
+          setRightCount={setRightCount}
+        />
+        <div className="tips">请输入正确的文字</div>
+        <div className="back" onClick={() => navigate("/")}>
+          <img src={require("../../assets/img/back.png")} alt="back" />
+        </div>
+        <div className="time">
+          <img src={require("../../assets/img/time.png")} alt="time" />
+          <div className="time-text">{dayjs().startOf('day').second(timeLeft).format('mm:ss')}</div>
+        </div>
+      </div>
+    )
+  );
+};
+
+export default QuestionList;

+ 159 - 0
src/page/rank/index.module.scss

@@ -0,0 +1,159 @@
+.rank {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  :global {
+    .title {
+      width: 100%;
+      height: 100px;
+      object-fit: contain;
+    }
+    .content {
+      width: 1260PX;
+      height: 590px;
+      background: url("../../assets/img/rank_bg.png") no-repeat center center;
+      background-size: 100% 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      .container {
+        width: 80%;
+        height: 73%;
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+        overflow: auto;
+        scrollbar-width: none;
+        -ms-overflow-style: none;
+        &::-webkit-scrollbar {
+          display: none;
+        }
+        .container-item {
+          width: fit-content;
+          height: 100%;
+          display: flex;
+          justify-content: flex-end;
+          align-items: center;
+          gap: 10px;
+          .rank-item {
+            width: 120px;
+            height: 100%;
+            background: url("../../assets/img/rank_item.png") no-repeat center center;
+            background-size: 100% 100%;
+            border-radius: 3px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            color: rgba(133, 95, 57, 1);
+            gap: 10px;
+            .rank-title {
+              width: 100%;
+              height: 11%;
+              font-size: 26px;
+              text-align: center;
+              line-height: 44px;
+              color: rgba(203, 181, 154, 1);
+            }
+            .nickname {
+              width: 26px;
+              height: 215px;
+              font-size: 26px;
+              text-align: center;
+              line-height: 34px;
+              word-break: break-all;
+            }
+            .score {
+              width: 100%;
+              height: 8%;
+              font-size: 22px;
+              text-align: center;
+              line-height: 44px;
+            }
+            .time {
+              width: 100%;
+              height: 8%;
+              font-size: 22px;
+              text-align: center;
+              line-height: 24px;
+            }
+            .current-rank {
+              opacity: 0;
+              width: 90%;
+              height: 8%;
+              font-size: 20px;
+              line-height: 33px;
+              text-align: center;
+            }
+          }
+          .rank-item-active {
+            width: 120px;
+            height: 100%;
+            background: url("../../assets/img/rank_current.png") no-repeat center center;
+            background-size: 100% 100%;
+            background-size: 100% 100%;
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+            .rank-title {
+              color: rgba(236, 118, 0, 1)
+            }
+            .nickname {
+              color: rgba(133, 95, 57, 1)
+            }
+            .score {
+              color: rgba(235, 146, 57, 1)
+            }
+            .time {
+              color: rgba(235, 146, 57, 1)
+            }
+            .current-rank {
+              opacity: 1;
+              background-color: rgba(235, 146, 57, 1);
+              color: rgba(255, 230, 206, 1)
+            }
+          }
+        }
+      } 
+      .left {
+        width: 5%;
+        height: 8%;
+        object-fit: contain;
+      }
+      .right {
+        width: 5%;
+        height: 8%;
+        object-fit: contain;
+      }
+    }
+    .btn-group {
+      width: 100%;
+      height: 100px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      gap: 40px;
+      font-size: 28px;
+      color: rgba(255, 240, 220, 1);
+      text-align: center;
+      line-height: 70px;
+      cursor: pointer;
+      .item {
+        width: 220px;
+        height: 70px;
+        background: url(../../assets/img/button_bg.png) no-repeat center center;
+        background-size: 100% 100%;
+      }
+      .backHome {
+        width: 220px;
+        height: 70px;
+        color: rgba(255, 240, 220, .5);
+        background: url(../../assets/img/button_bg2.png) no-repeat center center;
+        background-size: 100% 100%;
+      }
+
+    }
+  }
+}

+ 129 - 0
src/page/rank/index.tsx

@@ -0,0 +1,129 @@
+import React, { useEffect, useRef, useState } from "react";
+import styles from "./index.module.scss";
+import { useNavigate } from "react-router-dom";
+import { RankItem, getRankList } from "../../utils/API";
+
+const sortDESC = (arr: RankItem[]) => {
+  arr.sort((a, b) => {
+    if (a.score !== b.score) {
+      // 得分不同时,得分高的排在前面
+      return b.score - a.score;
+    } else {
+      // 得分相同时,花费时间少的排在前面
+      return a.time - b.time;
+    }
+  });
+  // 为排序后的数据添加排名信息
+  arr.forEach((item, index) => {
+    item.rank = index + 1;
+  });
+
+  // 降序
+  arr.reverse();
+  return arr;
+};
+
+const Rank = () => {
+  const navigate = useNavigate();
+  const [rankList, setRankList] = useState<RankItem[]>([]);
+  const [currentUser, setCurrentUser] = useState<number>(0);
+  const topThree = ["探花", "榜眼", "状元"];
+  const containerRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    getRankList().then((res) => {
+      setRankList(sortDESC(res.data.data));
+      // 暂时用最大id作为当前用户
+      setCurrentUser(res.data.data.find((item: RankItem) => item.id === res.data.data.length)!.id);
+    });
+    // 一开始就滚到当前用户
+    const container = document.querySelector(".rank-item-active");
+    if (container) {
+      container.scrollIntoView({ behavior: "smooth", inline: "start" });
+    }
+  }, []);
+
+  // 滚轮让x轴滚动
+  const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {
+    if (containerRef.current) {
+      containerRef.current.scrollTo({
+        left: containerRef.current.scrollLeft + event.deltaY,
+        behavior: "smooth",
+      });
+    }
+  };
+
+  // 移动端触摸滚动
+  let startX: number;
+
+  const touchStart = (event: React.TouchEvent<HTMLDivElement>) => {
+    if (containerRef.current) {
+      startX = event.touches[0].clientX; // 记录触摸开始的X坐标
+    }
+  };
+
+  const touchMove = (event: React.TouchEvent<HTMLDivElement>) => {
+    if (containerRef.current) {
+      const moveX = event.touches[0].clientX - startX;
+      containerRef.current.scrollTo({
+        left: containerRef.current.scrollLeft - moveX,
+        behavior: "smooth",
+      });
+      startX = event.touches[0].clientX;
+    }
+  };
+
+  const activeRank = rankList.find((item) => item.id === currentUser)?.rank!;
+  const handleLeft = () => {
+    if (activeRank < rankList.length) {
+      setCurrentUser(rankList.find((item) => item.rank === activeRank + 1)!.id);
+      if (containerRef.current) {
+        const target = containerRef.current.querySelector(".rank-item-active");
+        if (target) {
+          target.scrollIntoView({ behavior: "smooth", inline: "end" });
+        }
+      }
+    }
+  };
+
+  const handleRight = () => {
+    if (activeRank > 1) {
+      setCurrentUser(rankList.find((item) => item.rank === activeRank - 1)!.id);
+      if (containerRef.current) {
+        const target = containerRef.current.querySelector(".rank-item-active");
+        if (target) {
+          target.scrollIntoView({ behavior: "smooth", inline: "start" });
+        }
+      }
+    }
+  };
+
+  return (
+    <div className={styles.rank}>
+      <img className="title" src={require("../../assets/img/rank_title.png")} alt="rank_title" />
+      <div className="content">
+        <img className="left" onClick={handleLeft} src={require("../../assets/img/rank_left.png")} alt="rank_left" />
+        <div className="container" onWheel={handleWheel} onTouchStart={touchStart} onTouchMove={touchMove} >
+          <div className="container-item">
+            {rankList.map((item, index) => (
+              <div className={`rank-item ${item.id === currentUser ? "rank-item-active" : ""}`} key={index} >
+                <div className="rank-title">{index >= rankList.length - 3 ? topThree[index - (rankList.length - 3)] : `第${item.rank}名`} </div>
+                <div className="nickname">{item.name}</div>
+                <div className="score">获得{item.score}分</div>
+                <div className="time">耗时{item.time}秒</div>
+                <div className="current-rank">当前排名</div>
+              </div>
+            ))}
+          </div>
+        </div>
+        <img className="right" onClick={handleRight} src={require("../../assets/img/rank_right.png")} alt="rank_right" />
+      </div>
+      <div className="btn-group">
+        <div className="item " onClick={() => navigate("/")}>返回首页</div>
+        <div className="item" onClick={() => navigate("/question")}>重新开始</div>
+      </div>
+    </div>
+  );
+};
+
+export default Rank;

+ 0 - 1
src/react-app-env.d.ts

@@ -1 +0,0 @@
-/// <reference types="react-scripts" />

+ 0 - 15
src/reportWebVitals.ts

@@ -1,15 +0,0 @@
-import { ReportHandler } from 'web-vitals';
-
-const reportWebVitals = (onPerfEntry?: ReportHandler) => {
-  if (onPerfEntry && onPerfEntry instanceof Function) {
-    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
-      getCLS(onPerfEntry);
-      getFID(onPerfEntry);
-      getFCP(onPerfEntry);
-      getLCP(onPerfEntry);
-      getTTFB(onPerfEntry);
-    });
-  }
-};
-
-export default reportWebVitals;

+ 0 - 5
src/setupTests.ts

@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';

+ 8 - 0
src/type/declaration.d.ts

@@ -0,0 +1,8 @@
+declare module 'history'
+declare module '*.scss'
+declare module '*.png'
+declare module '*.jpg'
+declare module '*.gif'
+declare module '*.svg'
+declare module 'js-export-excel'
+declare module 'braft-utils'

+ 36 - 0
src/utils/API.ts

@@ -0,0 +1,36 @@
+import http from "./http";
+
+export interface RankItem {
+  createTime: string;
+  creatorId: null;
+  creatorName: string;
+  id: number;
+  name: string;
+  phone: string;
+  score: number;
+  time: number;
+  updateTime: string;
+  rank: number;
+}
+
+export interface RankDescItem {
+  score: number;
+  time: number;
+  rank: number;
+  isCurrentUser?: boolean;
+}
+
+export interface SaveScoreData {
+  name: string;
+  phone: string;
+  score: number;
+  time: number;
+}
+
+export const getRankList = () => {
+  return http.get("/api/show/score/sort");
+};
+
+export const saveScore = (data: SaveScoreData) => {
+  return http.post("/api/show/score/save", data);
+};

+ 8 - 0
src/utils/http.ts

@@ -0,0 +1,8 @@
+import axios from "axios";
+
+const http = axios.create({
+  baseURL: "http://192.168.20.61:8098/",
+  timeout: 5000,
+});
+
+export default http;

+ 20 - 0
src/utils/storage.ts

@@ -0,0 +1,20 @@
+// 封装sessionStorage
+export const setSessionStorage = (key: string, value: any) => {
+  sessionStorage.setItem(key, JSON.stringify(value))
+}
+
+export const getSessionStorage = (key: string) => {
+  if (sessionStorage.getItem(key) === '') {
+    return ''
+  }
+  return JSON.parse(sessionStorage.getItem(key) as string)
+}
+
+export const removeSessionStorage = (key: string) => {
+  sessionStorage.removeItem(key)
+}
+
+export const clearSessionStorage = () => {
+  sessionStorage.clear()
+}
+