Преглед изворни кода

小游戏: 企业翻翻看 本地功能

任一存 пре 2 година
родитељ
комит
818bc472e3
79 измењених фајлова са 7875 додато и 0 уклоњено
  1. 4 0
      game/.browserslistrc
  2. 0 0
      game/.env
  3. 3 0
      game/.env.dev
  4. 3 0
      game/.env.mytest
  5. 3 0
      game/.env.prod
  6. 3 0
      game/.eslintignore
  7. 56 0
      game/.eslintrc.js
  8. 24 0
      game/.gitignore
  9. 50 0
      game/README.md
  10. 9 0
      game/babel.config.js
  11. 20 0
      game/jsconfig.json
  12. 37 0
      game/package.json
  13. 22 0
      game/public/index.html
  14. BIN
      game/public/logo.png
  15. 139 0
      game/src/App.vue
  16. 17 0
      game/src/api.js
  17. BIN
      game/src/assets/images/icon_bonus_point.png
  18. BIN
      game/src/assets/images/icon_home.png
  19. BIN
      game/src/assets/images/icon_rules.png
  20. BIN
      game/src/assets/images/icon_time_count.png
  21. BIN
      game/src/assets/images/pair-up-bg.jpg
  22. BIN
      game/src/assets/images/pair-up-btn-restart.png
  23. BIN
      game/src/assets/images/pair-up-card-back-side.png
  24. BIN
      game/src/assets/images/pair-up-card-front-side.png
  25. BIN
      game/src/assets/images/pair-up-logos/万家基金管理有限公司.png
  26. BIN
      game/src/assets/images/pair-up-logos/上海东方证券资产管理有限公司.png
  27. BIN
      game/src/assets/images/pair-up-logos/上海证券交易所公益基金会.png
  28. BIN
      game/src/assets/images/pair-up-logos/东方财富证券股份有限公司.gif
  29. BIN
      game/src/assets/images/pair-up-logos/东海证券股份有限公司.jpg
  30. BIN
      game/src/assets/images/pair-up-logos/中信期货有限公司.png
  31. BIN
      game/src/assets/images/pair-up-logos/中信证券股份有限公司.png
  32. BIN
      game/src/assets/images/pair-up-logos/中国期货市场监控中心有限责任公司.png
  33. BIN
      game/src/assets/images/pair-up-logos/中国证券登记结算有限责任公司.png
  34. BIN
      game/src/assets/images/pair-up-logos/中国金融期货交易所.jpg
  35. BIN
      game/src/assets/images/pair-up-logos/中欧基金管理有限公司.png
  36. BIN
      game/src/assets/images/pair-up-logos/中海基金管理有限公司.png
  37. BIN
      game/src/assets/images/pair-up-logos/中航证券有限公司.png
  38. BIN
      game/src/assets/images/pair-up-logos/中证数据有限责任公司.png
  39. BIN
      game/src/assets/images/pair-up-logos/兴业证券股份有限公司.png
  40. BIN
      game/src/assets/images/pair-up-logos/兴证全球基金管理有限公司.png
  41. BIN
      game/src/assets/images/pair-up-logos/创金合信基金管理有限公司.png
  42. BIN
      game/src/assets/images/pair-up-logos/北京中金公益基金会.png
  43. BIN
      game/src/assets/images/pair-up-logos/华安基金管理有限公司.png
  44. BIN
      game/src/assets/images/pair-up-logos/华泰证券股份有限公司.jpg
  45. BIN
      game/src/assets/images/pair-up-logos/南华期货股份有限公司.png
  46. BIN
      game/src/assets/images/pair-up-logos/国元证券股份有限公司.png
  47. BIN
      game/src/assets/images/pair-up-logos/国盛证券有限责任公司.png
  48. BIN
      game/src/assets/images/pair-up-logos/圆信永丰基金管理有限公司.png
  49. BIN
      game/src/assets/images/pair-up-logos/富国基金管理有限公司.png
  50. BIN
      game/src/assets/images/pair-up-logos/平安证券股份有限公司.png
  51. BIN
      game/src/assets/images/pair-up-logos/广发证券股份有限公司.png
  52. BIN
      game/src/assets/images/pair-up-logos/德邦基金管理有限公司.png
  53. BIN
      game/src/assets/images/pair-up-logos/朱雀基金管理有限公司.png
  54. BIN
      game/src/assets/images/pair-up-logos/汇添富基金管理股份有限公司.jpg
  55. BIN
      game/src/assets/images/pair-up-logos/泰达宏利基金.png
  56. BIN
      game/src/assets/images/pair-up-logos/深圳市银华公益基金会.jpg
  57. BIN
      game/src/assets/images/pair-up-logos/睿远公益基金会.jpg
  58. BIN
      game/src/assets/images/pair-up-logos/郑州商品交易所.jpg
  59. BIN
      game/src/assets/images/pair-up-logos/金信期货有限公司.png
  60. BIN
      game/src/assets/images/pair-up-logos/长江证券股份有限公司.png
  61. BIN
      game/src/assets/images/pair-up-logos/鹏华基金管理有限公司.png
  62. BIN
      game/src/assets/images/pair-up-logos/鹏扬基金管理有限公司.jpg
  63. BIN
      game/src/assets/images/pair-up-over-star.png
  64. BIN
      game/src/assets/images/pair-up-title.png
  65. 59 0
      game/src/assets/style/my-reset.css
  66. 48 0
      game/src/assets/style/reset.css
  67. 132 0
      game/src/components/PairUpOver.vue
  68. 2 0
      game/src/config.js
  69. 44 0
      game/src/directives/v-click-outside.js
  70. 4 0
      game/src/libs/ua-parser.min.js
  71. 100 0
      game/src/main.js
  72. 31 0
      game/src/router/index.js
  73. 24 0
      game/src/store/index.js
  74. 30 0
      game/src/useFunctions/useSizeAdapt.js
  75. 70 0
      game/src/utils.js
  76. 24 0
      game/src/views/HomeView.vue
  77. 439 0
      game/src/views/PairUp.vue
  78. 38 0
      game/vue.config.js
  79. 6440 0
      game/yarn.lock

+ 4 - 0
game/.browserslistrc

@@ -0,0 +1,4 @@
+> 1%
+last 2 versions
+not dead
+not ie 11


+ 3 - 0
game/.env.dev

@@ -0,0 +1,3 @@
+VUE_APP_CLI_MODE=dev
+NODE_ENV=development
+PUBLIC_PATH=/

+ 3 - 0
game/.env.mytest

@@ -0,0 +1,3 @@
+VUE_APP_CLI_MODE=test
+NODE_ENV=production
+PUBLIC_PATH=./

+ 3 - 0
game/.env.prod

@@ -0,0 +1,3 @@
+VUE_APP_CLI_MODE=prod
+NODE_ENV=production
+PUBLIC_PATH=./

+ 3 - 0
game/.eslintignore

@@ -0,0 +1,3 @@
+*/libs
+/node_modules
+/.vscode

+ 56 - 0
game/.eslintrc.js

@@ -0,0 +1,56 @@
+module.exports = {
+  root: true,
+  env: {
+    browser: true,
+    commonjs: true,
+    es6: true,
+    jest: true,
+    jquery: true,
+    node: true,
+  },
+  'extends': [
+    'plugin:vue/vue3-recommended',
+    'eslint:recommended'
+  ],
+  parserOptions: {
+    parser: '@babel/eslint-parser'
+  },
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'semi': ['error', 'never'],
+    "no-unused-vars": ["warn", {
+      "vars": "all",
+      "args": "after-used",
+      "ignoreRestSiblings": false
+    }],
+    "keyword-spacing": ["error", { "before": true, "after": true }],
+    "object-curly-spacing": ["error", "always"],
+    "space-infix-ops": ["error"],
+    'key-spacing': ["error", {
+      "mode": "strict"
+    }],
+    "comma-spacing": ["error", { "before": false, "after": true }],
+    "func-call-spacing": ["error", "never"],
+    "semi-spacing": ["error", { "before": false, "after": true }],
+    "space-before-blocks": ["error", "always"],
+    'no-trailing-spaces': 'error',
+    'no-multi-spaces': 'error',
+    "indent": ["error", 2],
+    'no-empty': 'off',
+    // 默认不启用:为了避免细微的 bug,最好直接从 Object.prototype 调用挂载于prototype上的方法方法。例如,foo.hasOwnProperty("bar") 应该替换为 Object.prototype.hasOwnProperty.call(foo, "bar")。
+    'no-prototype-builtins': "off",
+  },
+  globals: {
+    api: true,
+    config: true,
+    mapState: true,
+    mapGetters: true,
+    mapMutations: true,
+    store: true,
+    utils: true,
+    useSizeAdapt: true,
+    defineProps: true,
+    defineEmits: true,
+  }
+}

+ 24 - 0
game/.gitignore

@@ -0,0 +1,24 @@
+.DS_Store
+node_modules
+/dist
+
+*.zip
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 50 - 0
game/README.md

@@ -0,0 +1,50 @@
+## 部署测试环境
+文件存放位置:
+
+访问url:
+
+## 笔记
+
+## 管美术要:
+底部按钮背景图
+
+底部数字字体文件
+
+翻翻看 card不同种类
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 9 - 0
game/babel.config.js

@@ -0,0 +1,9 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset',
+  ],
+  // plugins: [
+  //   "transform-object-rest-spread",
+  //   "@babel/plugin-proposal-optional-chaining",
+  // ],
+}

+ 20 - 0
game/jsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "esnext",
+    "baseUrl": "./",
+    "moduleResolution": "node",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  },
+  "exclude": ["libs", "node_modules"]
+}

+ 37 - 0
game/package.json

@@ -0,0 +1,37 @@
+{
+  "name": "my-app",
+  "version": "0.0.1",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve --mode dev",
+    "build-test": "vue-cli-service build --mode mytest",
+    "build-prod": "vue-cli-service build --mode prod",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@vueuse/core": "^10.4.1",
+    "axios": "^1.1.3",
+    "core-js": "^3.8.3",
+    "dayjs": "^1.11.7",
+    "lodash": "^4.17.21",
+    "mitt": "^3.0.0",
+    "v-viewer": "^3.0.11",
+    "viewerjs": "^1.11.6",
+    "vue-router": "^4.0.3",
+    "vue": "^3.2.13",
+    "vuex": "^4.0.0"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.12.16",
+    "@babel/eslint-parser": "^7.12.16",
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@vue/cli-plugin-router": "~5.0.0",
+    "@vue/cli-plugin-vuex": "~5.0.0",
+    "@vue/cli-service": "~5.0.0",
+    "eslint": "^7.32.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "less": "^4.0.0",
+    "less-loader": "^8.0.0"
+  }
+}

+ 22 - 0
game/public/index.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>logo.png">
+    <title>证券博物馆小游戏</title>
+  </head>
+  <body>
+    <!-- <script src="https://cdn.bootcss.com/vConsole/3.2.0/vconsole.min.js"></script> -->
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+
+    <script>
+      // new VConsole()
+    </script>
+  </body>
+</html>

BIN
game/public/logo.png


+ 139 - 0
game/src/App.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="top-wrapper">
+    <router-view />
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from "vue"
+import { useRoute, useRouter } from "vue-router"
+import { useStore } from "vuex"
+
+const {
+  windowSizeInCssForRef,
+  windowSizeWhenDesignForRef,
+} = useSizeAdapt(390, 752)
+
+const route = useRoute()
+const router = useRouter()
+const store = useStore()
+</script>
+
+<style lang="less">
+html, body {
+  overscroll-behavior: none;
+  overflow: hidden;
+  height: 100%;
+}
+
+// * {
+//   user-select: none;
+//   -webkit-touch-callout: none;
+// }
+
+#app {
+  height: 100%;
+  position: relative;
+  left: 50%;
+  top: 0;
+  transform: translate(-50%, 0);
+}
+
+// // 360浏览器不支持not()
+// input, textarea {
+//   user-select: initial;
+// }
+
+// 字体
+// @font-face {
+//   font-family: 'Source Han Serif CN';
+//   src: url('@/assets/style/SourceHanSerifCN-Regular.otf');
+// }
+// @font-face {
+//   font-family: 'Source Han Serif CN-Bold';
+//   src: url('@/assets/style/SourceHanSerifCN-Bold.otf');
+// }
+// i {
+//   font-style: italic;
+// }
+
+// 滚动条,只设置某一项可能导致不生效。
+// ::-webkit-scrollbar { background: #dddecc; width: 6px; height: 6px; }
+// ::-webkit-scrollbar-thumb { background: #828a5b; border-radius: 3px; }
+// ::-webkit-scrollbar-corner { background: #dddecc; }
+
+// vue组件过渡效果
+.fade-out-leave-active {
+  transition: opacity 1s;
+  pointer-events: none;
+}
+.fade-out-leave-to {
+  opacity: 0;
+}
+
+// vue组件过渡效果
+.fade-in-enter-active {
+  transition: opacity 1s;
+}
+.fade-in-enter-from {
+  opacity: 0;
+}
+
+.fade-out-leave-active {
+  transition: opacity 1s;
+  pointer-events: none;
+}
+.fade-out-leave-to {
+  opacity: 0;
+}
+
+.fade-in-out-enter-active {
+  transition: opacity 2s;
+}
+.fade-in-out-leave-active {
+  transition: opacity 2s;
+  pointer-events: none;
+}
+.fade-in-out-enter-from {
+  opacity: 0;
+}
+.fade-in-out-leave-to {
+  opacity: 0;
+}
+
+// 不断渐变显隐 animation
+.animation-show-hide {
+  animation: show-hide 1.8s infinite;
+}
+@keyframes show-hide {
+  0% {
+    opacity: 0;
+  }
+  50% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0;
+  }
+}
+
+// // vue-viewer
+// .viewer-container {
+//   background-color: rgba(0, 0, 0, 80%) !important;
+// }
+// 或者
+// .viewer-backdrop {
+//   background-color: rgba(0, 0, 0, 90%) !important;
+// }
+</style>
+
+<style lang="less" scoped>
+.top-wrapper{
+  position: absolute;
+  left: 50%;
+  top: 0;
+  transform: translate(-50%, 0);
+  width: calc(390 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+  height: 100%;
+}
+</style>

+ 17 - 0
game/src/api.js

@@ -0,0 +1,17 @@
+// import axios from "axios"
+
+// axios({
+//   method: 'post',
+//   url: `${config.backendDir}visit/saveType`,
+//   headers: {
+//     appId: "CA02F83A5FA162B930AA2F962D202F43B0F6DE0B51AD79FEDB03FA8202BB4909330105B3B347510D87C97060C4288280D4A744E00565A4EC",
+//     "Content-Type": "application/json",
+//   },
+//   data: {
+//     moduleType,
+//     type: 'visit',
+//   },
+// })
+
+export default {
+}

BIN
game/src/assets/images/icon_bonus_point.png


BIN
game/src/assets/images/icon_home.png


BIN
game/src/assets/images/icon_rules.png


BIN
game/src/assets/images/icon_time_count.png


BIN
game/src/assets/images/pair-up-bg.jpg


BIN
game/src/assets/images/pair-up-btn-restart.png


BIN
game/src/assets/images/pair-up-card-back-side.png


BIN
game/src/assets/images/pair-up-card-front-side.png


BIN
game/src/assets/images/pair-up-logos/万家基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/上海东方证券资产管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/上海证券交易所公益基金会.png


BIN
game/src/assets/images/pair-up-logos/东方财富证券股份有限公司.gif


BIN
game/src/assets/images/pair-up-logos/东海证券股份有限公司.jpg


BIN
game/src/assets/images/pair-up-logos/中信期货有限公司.png


BIN
game/src/assets/images/pair-up-logos/中信证券股份有限公司.png


BIN
game/src/assets/images/pair-up-logos/中国期货市场监控中心有限责任公司.png


BIN
game/src/assets/images/pair-up-logos/中国证券登记结算有限责任公司.png


BIN
game/src/assets/images/pair-up-logos/中国金融期货交易所.jpg


BIN
game/src/assets/images/pair-up-logos/中欧基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/中海基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/中航证券有限公司.png


BIN
game/src/assets/images/pair-up-logos/中证数据有限责任公司.png


BIN
game/src/assets/images/pair-up-logos/兴业证券股份有限公司.png


BIN
game/src/assets/images/pair-up-logos/兴证全球基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/创金合信基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/北京中金公益基金会.png


BIN
game/src/assets/images/pair-up-logos/华安基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/华泰证券股份有限公司.jpg


BIN
game/src/assets/images/pair-up-logos/南华期货股份有限公司.png


BIN
game/src/assets/images/pair-up-logos/国元证券股份有限公司.png


BIN
game/src/assets/images/pair-up-logos/国盛证券有限责任公司.png


BIN
game/src/assets/images/pair-up-logos/圆信永丰基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/富国基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/平安证券股份有限公司.png


BIN
game/src/assets/images/pair-up-logos/广发证券股份有限公司.png


BIN
game/src/assets/images/pair-up-logos/德邦基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/朱雀基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/汇添富基金管理股份有限公司.jpg


BIN
game/src/assets/images/pair-up-logos/泰达宏利基金.png


BIN
game/src/assets/images/pair-up-logos/深圳市银华公益基金会.jpg


BIN
game/src/assets/images/pair-up-logos/睿远公益基金会.jpg


BIN
game/src/assets/images/pair-up-logos/郑州商品交易所.jpg


BIN
game/src/assets/images/pair-up-logos/金信期货有限公司.png


BIN
game/src/assets/images/pair-up-logos/长江证券股份有限公司.png


BIN
game/src/assets/images/pair-up-logos/鹏华基金管理有限公司.png


BIN
game/src/assets/images/pair-up-logos/鹏扬基金管理有限公司.jpg


BIN
game/src/assets/images/pair-up-over-star.png


BIN
game/src/assets/images/pair-up-title.png


+ 59 - 0
game/src/assets/style/my-reset.css

@@ -0,0 +1,59 @@
+* {
+  /* 阻止safari在用户交互设置一些元素的背景色 */
+  -webkit-tap-highlight-color: transparent;
+  box-sizing: border-box;
+}
+
+html {
+  overflow: hidden;
+  touch-action: none;
+  scroll-behavior: smooth; /* MDN: When this property is specified on the root element, it applies to the viewport instead. This property specified on the body element will not propagate to the viewport.(???) */
+  height: 100%;
+}
+
+body {
+  text-align: justify;
+  height: 100%;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+  color: initial;
+  text-decoration: initial;
+  outline: none;
+}
+
+button {
+  padding: 0;
+  cursor: pointer;
+  background-color: initial;
+  border: initial;
+  outline: none;
+  white-space: pre;
+}
+
+img {
+  user-select: none;
+}
+
+menu {
+  list-style-type: initial;
+}
+
+li {
+  display: initial;
+}
+
+input {
+  outline: initial;
+  background: initial;
+  border: initial;
+  border-radius: initial;
+  width: initial;
+  height: initial;
+}
+
+td {
+  vertical-align: inherit;
+}

+ 48 - 0
game/src/assets/style/reset.css

@@ -0,0 +1,48 @@
+/* http://meyerweb.com/eric/tools/css/reset/ 
+   v2.0 | 20110126
+   License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section {
+	display: block;
+}
+body {
+	line-height: 1;
+}
+ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}

+ 132 - 0
game/src/components/PairUpOver.vue

@@ -0,0 +1,132 @@
+<template>
+  <div class="pair-up-over">
+    <img
+      class="star"
+      src="@/assets/images/pair-up-over-star.png"
+      alt=""
+      draggable="false"
+    >
+    <h2>恭喜您!</h2>
+    <p class="corp-count">
+      您成功认识了<span class="number">{{ props.corpCount }}</span>家教育帮扶企业
+    </p>
+    <p class="bonus-count">
+      获得<span class="number">{{ props.bonusCount }}</span>积分
+    </p>
+    <button
+      class="replay"
+      @click="emit('replay')"
+    >
+      重新开始
+    </button>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from "vue"
+import { useRoute, useRouter } from "vue-router"
+import { useStore } from "vuex"
+
+const route = useRoute()
+const router = useRouter()
+const store = useStore()
+
+const {
+  windowSizeInCssForRef,
+  windowSizeWhenDesignForRef,
+} = useSizeAdapt(390, 752)
+
+const props = defineProps(['corpCount', 'bonusCount'])
+
+const emit = defineEmits(['replay'])
+
+</script>
+
+<style lang="less" scoped>
+.pair-up-over{
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  >img.star{
+    position: absolute;
+    left: 50%;
+    top: calc(100 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-50%, 0);
+    width: calc(300 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+  }
+  >h2{
+    position: absolute;
+    left: 50%;
+    top: calc(380 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-35%, 0);
+    font-size: calc(40 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    font-family: Source Han Sans SC, Source Han Sans SC;
+    font-weight: bold;
+    color: #FFFFFF;
+    line-height: calc(47 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+  }
+  >p.corp-count{
+    position: absolute;
+    left: 50%;
+    top: calc(440 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-50%, 0);
+    font-size: calc(16 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    font-family: Source Han Sans SC, Source Han Sans SC;
+    font-weight: 400;
+    color: #FFFFFF;
+    line-height: calc(19 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    white-space: pre;
+    >span.number{
+      font-size: calc(24 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      font-family: Source Han Sans SC, Source Han Sans SC;
+      font-weight: bold;
+      color: #EFB622;
+      line-height: calc(28 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      margin-left: 0.2em;
+      margin-right: 0.2em;
+    }
+  }
+  >p.bonus-count{
+    position: absolute;
+    left: 50%;
+    top: calc(480 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-50%, 0);
+    font-size: calc(16 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    font-family: Source Han Sans SC, Source Han Sans SC;
+    font-weight: 400;
+    color: #FFFFFF;
+    line-height: calc(19 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    white-space: pre;
+    >span.number{
+      font-size: calc(24 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      font-family: Source Han Sans SC, Source Han Sans SC;
+      font-weight: bold;
+      color: #EFB622;
+      line-height: calc(28 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      margin-left: 0.2em;
+      margin-right: 0.2em;
+    }
+  }
+  >button.replay{
+    position: absolute;
+    left: 50%;
+    bottom: calc(52 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-50%, 0);
+    width: calc(322 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    height: calc(92 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    background-image: url(@/assets/images/pair-up-btn-restart.png);
+    background-size: contain;
+    background-repeat: no-repeat;
+    background-position: center center;
+    font-size: calc(28 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    font-family: Source Han Sans SC, Source Han Sans SC;
+    font-weight: 800;
+    color: #FFFFFF;
+    line-height: calc(32 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    -webkit-text-stroke: calc(1 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef')) #4E3933;
+    padding-bottom: calc(5 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+  }
+}
+</style>

+ 2 - 0
game/src/config.js

@@ -0,0 +1,2 @@
+export default {
+}

+ 44 - 0
game/src/directives/v-click-outside.js

@@ -0,0 +1,44 @@
+export default {
+  install(app) {
+    app.directive('click-outside', {
+      mounted(el, binding) {
+        function documentHandler(e) {
+          if (el.contains(e.target)) {
+            return false
+          }
+          if (binding.value) {
+            binding.value(e)
+          }
+        }
+        el.__vueClickOutside__ = documentHandler
+        if (binding.modifiers.click) {
+          document.addEventListener('click', documentHandler, {
+            capture: binding.modifiers.capture
+          })
+        }
+        if (binding.modifiers.mousedown) {
+          document.addEventListener('mousedown', documentHandler, {
+            capture: binding.modifiers.capture
+          })
+        }
+        if (binding.modifiers.touchstart) {
+          document.addEventListener('touchstart', documentHandler, {
+            capture: binding.modifiers.capture
+          })
+        }
+      },
+      unMounted(el, binding) {
+        document.removeEventListener('click', el.__vueClickOutside__, {
+          capture: binding.modifiers.capture
+        })
+        document.removeEventListener('mousedown', el.__vueClickOutside__, {
+          capture: binding.modifiers.capture
+        })
+        document.removeEventListener('tarchstart', el.__vueClickOutside__, {
+          capture: binding.modifiers.capture
+        })
+        delete el.__vueClickOutside__
+      }
+    })
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 4 - 0
game/src/libs/ua-parser.min.js


+ 100 - 0
game/src/main.js

@@ -0,0 +1,100 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import "@/assets/style/reset.css"
+import "@/assets/style/my-reset.css"
+import UAParser from "@/libs/ua-parser.min.js"
+import clickOutside from "@/directives/v-click-outside.js"
+import mitt from "mitt"
+import 'viewerjs/dist/viewer.css'
+import VueViewer from 'v-viewer'
+
+console.log(`version: ${process.env.VUE_APP_VERSION}`)
+console.log(`Build time: ${process.env.VUE_APP_UPDATE_TIME}`)
+
+const app = createApp(App)
+
+// 挂载配置信息
+app.provide('$config', config)
+app.provide('$env', process.env)
+
+// 挂载消息发布订阅中心
+app.provide('$mitt', mitt())
+
+// 解析、挂载浏览器信息
+const uaParser = new UAParser()
+const uaInfo = uaParser.getResult()
+console.log('uaInfo: ', uaInfo)
+app.provide('$uaInfo', uaInfo)
+if (uaInfo.browser && uaInfo.browser.name === 'WeChat') {
+  app.provide('$isWeChat', true)
+}
+if (uaInfo.browser && uaInfo.browser.name === 'Safari') {
+  app.provide('$isSafari', true)
+}
+if (uaInfo.device.type === 'mobile') {
+  app.provide('$isMobile', true)
+}
+
+// 处理resize事件
+let windowWidthLast = window.innerWidth
+let windowHeightLast = window.innerHeight
+function onResize() {
+  if (window.innerWidth === windowWidthLast) {
+    // 发生了高度变化,认为发生了软键盘显隐
+    if (uaInfo.os.name === 'Android') {
+      if (window.innerHeight < windowHeightLast) {
+        // ...
+      } else if (window.innerHeight > windowHeightLast) {
+        // ...
+      }
+    }
+  }
+  windowWidthLast = window.innerWidth
+  windowHeightLast = window.innerHeight
+}
+window.addEventListener('resize', () => {
+  onResize()
+})
+
+// // 禁用上下文菜单
+// document.oncontextmenu = function(e) {
+//   e.preventDefault()
+// }
+
+// // safari里只能在交互行为的回调中成功地首次调用audio的play方法,所以需要一个全局的audio元素来播放随时可能需要自发播放的音频。
+// const audioNode = document.createElement("audio")
+// audioNode.id = 'global-audio'
+// audioNode.style.display = 'none'
+// audioNode.loop = true
+// document.body.appendChild(audioNode)
+
+app.use(store)
+  .use(router)
+  .use(clickOutside)
+  .use(VueViewer)
+  // .component('HotSpot', HotSpot)
+  .mount('#app')
+
+//  you can reset the default options at any other time
+VueViewer.setDefaults({
+  inline: false,
+  button: true,
+  navbar: false,
+  title: false,
+  toolbar: false,
+  tooltip: false,
+  movable: true,
+  zoomable: true,
+  rotatable: false,
+  // "scalable": true,
+  transition: true,
+  fullscreen: true,
+  keyboard: true,
+})
+
+// 必须在vue根组件挂载之后执行
+if (uaInfo.device.type === 'mobile') {
+  document.getElementById('app').classList.add('mobile')
+}

+ 31 - 0
game/src/router/index.js

@@ -0,0 +1,31 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import HomeView from '../views/HomeView.vue'
+import PairUp from '../views/PairUp.vue'
+// import store from '@/store/index.js'
+
+const routes = [
+  {
+    path: '/',
+    name: 'HomeView',
+    component: HomeView,
+  },
+  {
+    path: '/pair-up',
+    name: 'PairUp',
+    component: PairUp,
+  }
+]
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+
+router.beforeEach((to, from) => {
+  // 生产环境下强制每次都从首页进入
+  if (process.env.NODE_ENV === 'production' && !from.name && to.name !== 'HomeView') {
+    return '/'
+  }
+})
+
+export default router

+ 24 - 0
game/src/store/index.js

@@ -0,0 +1,24 @@
+import { createStore } from 'vuex'
+
+export default createStore({
+  state: {
+    loginStatus: 0, // 0: 未登录;1:已登录
+    avatar: '',
+    userName: '',
+    bonusPoint: 0,
+  },
+  getters: {
+  },
+  mutations: {
+    setUsingChinese(state, value) {
+      state.usingChinese = value
+    },
+    changeBonusPoint(state, delta) {
+      state.avatar += delta
+    },
+  },
+  actions: {
+  },
+  modules: {
+  }
+})

+ 30 - 0
game/src/useFunctions/useSizeAdapt.js

@@ -0,0 +1,30 @@
+// 用法:
+// width: calc(950 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+
+import { onBeforeUnmount, ref } from 'vue'
+
+export default function useSizeAdapt(windowWidthWhenDesign = 1920, windowHeightWhenDesign = 1080) {
+  const windowSizeInCssForRef = ref('')
+  const windowSizeWhenDesignForRef = ref('')
+
+  function compute() {
+    if (window.innerWidth / window.innerHeight > windowWidthWhenDesign / windowHeightWhenDesign) {
+      windowSizeWhenDesignForRef.value = windowHeightWhenDesign
+      windowSizeInCssForRef.value = window.innerHeight + 'px' // 用100vh的话在移动端会有兼容性问题。
+    } else {
+      windowSizeWhenDesignForRef.value = windowWidthWhenDesign
+      windowSizeInCssForRef.value = '100vw'
+    }
+  }
+
+  compute()
+  window.addEventListener('resize', compute)
+  onBeforeUnmount(() => {
+    window.removeEventListener('resize', compute)
+  })
+
+  return {
+    windowSizeInCssForRef,
+    windowSizeWhenDesignForRef,
+  }
+}

+ 70 - 0
game/src/utils.js

@@ -0,0 +1,70 @@
+export default {
+  /**
+   * 返回一个自带消抖效果的函数,用res表示。
+   *
+   * fn: 需要被消抖的函数
+   * delay: 消抖时长
+   * isImmediateCall: 是否在一组操作中的第一次调用时立即执行fn
+   * isRememberLastCall:是否在一组中最后一次调用后等delay时长再执行fn
+   */
+  debounce(fn, delay = 250, isImmediateCall = true, isRememberLastCall = true) {
+    console.assert(isImmediateCall || isRememberLastCall, 'isImmediateCall 和 isRememberLastCall 至少应有一个是true,否则没有意义!')
+    let timer = null
+    // 上次调用的时刻
+    let lastCallTime = 0
+
+    if (isImmediateCall && !isRememberLastCall) {
+      return function (...args) {
+        const currentTime = Date.now()
+        if (currentTime - lastCallTime >= delay) {
+          fn.apply(this, args)
+        }
+        lastCallTime = currentTime
+      }
+    } else if (!isImmediateCall && isRememberLastCall) {
+      return function (...args) {
+        if (timer) {
+          clearTimeout(timer)
+        }
+        timer = setTimeout(() => {
+          fn.apply(this, args)
+        }, delay)
+      }
+    } else if (isImmediateCall && isRememberLastCall) {
+      return function (...args) {
+        const currentTime = Date.now()
+        if (currentTime - lastCallTime >= delay) { // 一组操作中的第一次
+          fn.apply(this, args)
+          lastCallTime = currentTime
+          return
+        } else { // 一组中的后续调用
+          if (timer) { // 在此之前存在中间调用
+            lastCallTime = currentTime
+            clearTimeout(timer)
+          }
+          timer = setTimeout(() => {
+            fn.apply(this, args)
+            lastCallTime = 0
+            timer = null
+          }, delay)
+        }
+      }
+    } else {
+      console.error('不应该执行到这里!')
+    }
+  },
+  throttle(fn, interval = 250) {
+    let lastRunTime = 0
+
+    return function (...args) {
+      let elapsedTime = Date.now() - lastRunTime
+      if (elapsedTime < interval) {
+        return null
+      }
+
+      let context = this
+      lastRunTime = Date.now()
+      return fn.apply(context, args)
+    }
+  },
+}

+ 24 - 0
game/src/views/HomeView.vue

@@ -0,0 +1,24 @@
+<template>
+  <div
+    class="home"
+  >
+    sdfsf
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from "vue"
+import { useRoute, useRouter } from "vue-router"
+import { useStore } from "vuex"
+
+const route = useRoute()
+const router = useRouter()
+const store = useStore()
+</script>
+
+<style lang="less" scoped>
+.home {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 439 - 0
game/src/views/PairUp.vue

@@ -0,0 +1,439 @@
+<template>
+  <div class="game-view">
+    <img
+      class="title"
+      src="@/assets/images/pair-up-title.png"
+      alt=""
+      draggable="false"
+    >
+    <div
+      v-show="!isOver"
+      class="tip"
+    >
+      请翻开相同的企业logo
+    </div>
+    <!-- 卡牌列表 -->
+    <div
+      v-show="!isOver"
+      class="card-list"
+    >
+      <div
+        v-for="(card, cardIdx) in cardList"
+        :key="cardIdx"
+        class="card"
+        :class="{
+          active: activeCardIdxList.includes(cardIdx),
+          hide: card.isDone,
+        }"
+        @click="onClickCard(card, cardIdx)"
+      >
+        <div class="front">
+          <img
+            class="card-frame"
+            src="@/assets/images/pair-up-card-front-side.png"
+            alt=""
+            draggable="false"
+          >
+          <img
+            class="card-content"
+            :src="require(`@/assets/images/pair-up-logos/${logoFileNameList[card.logoIdx]}`)"
+            alt=""
+            draggable="false"
+          >
+        </div>
+        <img
+          class="back"
+          src="@/assets/images/pair-up-card-back-side.png"
+          alt=""
+          draggable="false"
+        >
+      </div>
+    </div>
+    <!-- 左上角按钮 -->
+    <div class="btn-group">
+      <button
+        class="return-home"
+        @click="onClickReturnHome"
+      />
+      <button class="game-rule" />
+    </div>
+    <!-- 底部信息栏 -->
+    <div
+      v-show="!isOver"
+      class="common-info-group"
+    >
+      <div class="info-item bonus-point">
+        <img
+          class="icon"
+          src="@/assets/images/icon_bonus_point.png"
+          alt=""
+          draggable="false"
+        >
+        <span class="number">{{ bonusPoint }}</span>
+      </div>
+      <div class="info-item time-count">
+        <img
+          class="icon"
+          src="@/assets/images/icon_time_count.png"
+          alt=""
+          draggable="false"
+        >
+        <span class="number">{{ timeCountForShow }}</span>
+      </div>
+    </div>
+    <PairUpOver
+      v-show="isOver"
+      :corp-count="recognizedCorpList.length"
+      :bonus-count="bonusPoint"
+      @replay="replay"
+    />
+  </div>
+</template>
+
+<script setup>
+import useSizeAdapt from "@/useFunctions/useSizeAdapt"
+import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue"
+import { useRoute, useRouter } from "vue-router"
+import { useStore } from "vuex"
+import dayjs from 'dayjs'
+import { shuffle } from 'lodash'
+import PairUpOver from '@/components/PairUpOver.vue'
+
+const route = useRoute()
+const router = useRouter()
+const store = useStore()
+
+const {
+  windowSizeInCssForRef,
+  windowSizeWhenDesignForRef,
+} = useSizeAdapt(390, 752)
+
+function onClickReturnHome() {
+  router.push({
+    name: 'HomeView',
+  })
+}
+
+const isOver = ref(false)
+
+/**
+ * 倒计时
+ */
+const timeCount = ref(120)
+let timeCountIntervalId = null
+timeCountIntervalId = setInterval(() => {
+  timeCount.value--
+  if (timeCount.value === 0) {
+    clearInterval(timeCountIntervalId)
+    isOver.value = true
+  }
+}, 1000)
+onBeforeUnmount(() => {
+  clearInterval(timeCountIntervalId)
+})
+const timeCountForShow = computed(() => {
+  return dayjs(timeCount.value * 1000).format('m:ss')
+})
+
+/**
+ * 本次游戏积分
+ */
+const bonusPoint = ref(0)
+
+/**
+ * 本次游戏识别企业数
+ */
+const recognizedCorpList = ref([])
+
+/**
+ * 重玩
+ */
+function replay() {
+  isOver.value = false
+  timeCount.value = 10
+  timeCountIntervalId = setInterval(() => {
+    timeCount.value--
+    if (timeCount.value === 0) {
+      clearInterval(timeCountIntervalId)
+      isOver.value = true
+    }
+  }, 1000)
+  recognizedCorpList.value = 0
+}
+
+/**
+ * 具体游戏规则
+ */
+const logoFileNameList = [
+  '中海基金管理有限公司.png',
+  '中航证券有限公司.png',
+  '中欧基金管理有限公司.png',
+  '中信期货有限公司.png',
+  '中信证券股份有限公司.png',
+  '中证数据有限责任公司.png',
+  '朱雀基金管理有限公司.png',
+  '北京中金公益基金会.png',
+  '创金合信基金管理有限公司.png',
+  '德邦基金管理有限公司.png',
+  '东方财富证券股份有限公司.gif',
+  '东海证券股份有限公司.jpg',
+  '富国基金管理有限公司.png',
+  '广发证券股份有限公司.png',
+  '国盛证券有限责任公司.png',
+  '国元证券股份有限公司.png',
+  '华安基金管理有限公司.png',
+  '华泰证券股份有限公司.jpg',
+  '汇添富基金管理股份有限公司.jpg',
+  '金信期货有限公司.png',
+  '南华期货股份有限公司.png',
+  '鹏华基金管理有限公司.png',
+  '鹏扬基金管理有限公司.jpg',
+  '平安证券股份有限公司.png',
+  '睿远公益基金会.jpg',
+  '上海东方证券资产管理有限公司.png',
+  '上海证券交易所公益基金会.png',
+  '深圳市银华公益基金会.jpg',
+  '泰达宏利基金.png',
+  '万家基金管理有限公司.png',
+  '兴业证券股份有限公司.png',
+  '兴证全球基金管理有限公司.png',
+  '圆信永丰基金管理有限公司.png',
+  '长江证券股份有限公司.png',
+  '郑州商品交易所.jpg',
+  '中国金融期货交易所.jpg',
+  '中国期货市场监控中心有限责任公司.png',
+  '中国证券登记结算有限责任公司.png',
+]
+const cardList = ref([])
+function setCardList() {
+  cardList.value = []
+  nextTick(() => {
+    for (let index = 0; index < 8; index++) {
+      const logoIdx = Math.floor(Math.random() * logoFileNameList.length)
+      cardList.value.push({
+        logoIdx,
+        isDone: false,
+      })
+      cardList.value.push({
+        logoIdx,
+        isDone: false,
+      })
+    }
+    if (process.env.VUE_APP_CLI_MODE !== 'dev') {
+      cardList.value = shuffle(cardList.value)
+    }
+  })
+}
+setCardList()
+
+const activeCardIdxList = ref([])
+const activeCardIdxListRealTime = ref([])
+function onClickCard(card, cardIdx) {
+  if (activeCardIdxList.value.includes(cardIdx)) {
+    return
+  }
+  activeCardIdxList.value.push(cardIdx)
+  activeCardIdxListRealTime.value.push(cardIdx)
+  if (activeCardIdxListRealTime.value.length === 2 && cardList.value[activeCardIdxListRealTime.value[0]].logoIdx === cardList.value[activeCardIdxListRealTime.value[1]].logoIdx) {
+    const logoIdx = cardList.value[activeCardIdxListRealTime.value[0]].logoIdx
+    const toDeleteValue1 = activeCardIdxListRealTime.value.shift()
+    const toDeleteValue2 = activeCardIdxListRealTime.value.shift()
+    setTimeout(() => {
+      cardList.value[toDeleteValue1].isDone = true
+      cardList.value[toDeleteValue2].isDone = true
+      bonusPoint.value += 1
+      if (!recognizedCorpList.value.includes(logoIdx)) {
+        recognizedCorpList.value.push(logoIdx)
+      }
+      setTimeout(() => {
+        const toDeleteIdx1 = activeCardIdxList.value.findIndex((item) => {
+          return item === toDeleteValue1
+        })
+        activeCardIdxList.value.splice(toDeleteIdx1, 1)
+        const toDeleteIdx2 = activeCardIdxList.value.findIndex((item) => {
+          return item === toDeleteValue2
+        })
+        activeCardIdxList.value.splice(toDeleteIdx2, 1)
+
+        if (cardList.value.every((item) => {
+          return item.isDone
+        })) {
+          setCardList()
+        }
+      }, 400)
+    }, 400)
+  } else if (activeCardIdxListRealTime.value.length === 3) {
+    const toDeleteValue1 = activeCardIdxListRealTime.value.shift()
+    const toDeleteValue2 = activeCardIdxListRealTime.value.shift()
+    setTimeout(() => {
+      const toDeleteIdx1 = activeCardIdxList.value.findIndex((item) => {
+        return item === toDeleteValue1
+      })
+      activeCardIdxList.value.splice(toDeleteIdx1, 1)
+      const toDeleteIdx2 = activeCardIdxList.value.findIndex((item) => {
+        return item === toDeleteValue2
+      })
+      activeCardIdxList.value.splice(toDeleteIdx2, 1)
+    }, 400)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.game-view{
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background-image: url(@/assets/images/pair-up-bg.jpg);
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center center;
+  >img.title{
+    position: absolute;
+    left: 50%;
+    top: calc(35 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-50%, 0);
+    width: calc(244 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    height: calc(80 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+
+  }
+  >.tip{
+    position: absolute;
+    left: 50%;
+    top: calc(122 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-50%, 0);
+    font-size: calc(14 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    font-family: Source Han Sans SC, Source Han Sans SC;
+    font-weight: 400;
+    color: #FFFFFF;
+    line-height: calc(16 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+  }
+  >.card-list{
+    position: absolute;
+    top: calc(163 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    left: calc(31 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    right: calc((31 - 9) / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    margin-right: calc(-12 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    >.card{
+      position: relative;
+      display: inline-block;
+      margin-right: calc(9 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      margin-bottom: calc(9 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      width: calc(75 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      height: calc(115 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      border-radius: calc(3 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      cursor: pointer;
+      transition: transform 0.4s;
+      >.front{
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        transition: transform 0.4s;
+        transform: rotateY(180deg);
+        >img.card-frame{
+          position: absolute;
+          left: 0;
+          top: 0;
+          width: 100%;
+          height: 100%;
+        }
+        >img.card-content{
+          position: absolute;
+          left: 50%;
+          top: 50%;
+          transform: translate(-50%, -50%);
+          width: 70%;
+        }
+      }
+      >img.back{
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        backface-visibility: hidden;
+        transition: transform 0.4s;
+      }
+    }
+    >.card.active{
+      >.front{
+        transform: rotateY(0);
+      }
+      >img.back{
+        transform: rotateY(180deg);
+      }
+    }
+    >.card.hide{
+      pointer-events: none;
+      transform: scale(0) rotateZ(360deg);
+    }
+  }
+  >.btn-group{
+    position: absolute;
+    top: calc(36 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    right: calc(12 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    display: flex;
+    flex-direction: column;
+    gap: calc(16 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    >button.return-home{
+      background-image: url(@/assets/images/icon_home.png);
+      background-size: contain;
+      background-repeat: no-repeat;
+      background-position: center center;
+      width: calc(40 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      height: calc(40 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    }
+    >button.game-rule{
+      background-image: url(@/assets/images/icon_rules.png);
+      background-size: contain;
+      background-repeat: no-repeat;
+      background-position: center center;
+      width: calc(40 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      height: calc(40 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    }
+  }
+  >.common-info-group{
+    position: absolute;
+    left: 50%;
+    bottom: calc(41 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    transform: translate(-50%, 0);
+    display: flex;
+    gap: calc(11 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    >.info-item{
+      position: relative;
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-end;
+      width: calc(145 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      height: calc(31 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      padding-left: calc(5 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      padding-right: calc(18 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      >.icon{
+      }
+      >.number{
+        font-size: calc(20 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+        font-family: Inter, Inter;
+        font-weight: 400;
+        color: #FFFFFF;
+        line-height: calc(23 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      }
+    }
+    >.bonus-point{
+      >.icon{
+        width: calc(46 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+        height: calc(41 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      }
+    }
+    >.time-count{
+      >.icon{
+        width: calc(37 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+        height: calc(46 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      }
+    }
+  }
+}
+</style>

+ 38 - 0
game/vue.config.js

@@ -0,0 +1,38 @@
+const webpack = require('webpack')
+const { defineConfig } = require('@vue/cli-service')
+
+process.env.VUE_APP_VERSION = require('./package.json').version
+
+const dayjs = require('dayjs')
+const time = dayjs().format('YYYY-M-D HH:mm:ss')
+process.env.VUE_APP_UPDATE_TIME = time
+
+module.exports = defineConfig({
+  publicPath: process.env.PUBLIC_PATH,
+  productionSourceMap: process.env.VUE_APP_CLI_MODE === 'prod' ? false : true,
+  // transpileDependencies: true, // 默认false,表示babel-loader 会忽略所有 node_modules 中的文件
+  configureWebpack: {
+    module: {
+      rules: [
+        // {
+        //   test: /\.cur$/,
+        //   use: {
+        //     loader: 'file-loader'
+        //   }
+        // }
+      ]
+    },
+    plugins: [
+      new webpack.ProvidePlugin({
+        utils: ['/src/utils.js', 'default'],
+        store: ['/src/store/index.js', 'default'],
+        api: ['/src/api.js', 'default'],
+        config: ['/src/config.js', 'default'],
+        mapState: ['vuex', 'mapState'],
+        mapGetters: ['vuex', 'mapGetters'],
+        mapMutations: ['vuex', 'mapMutations'],
+        useSizeAdapt: ['/src/useFunctions/useSizeAdapt.js', 'default'],
+      }),
+    ],
+  },
+})

Разлика између датотеке није приказан због своје велике величине
+ 6440 - 0
game/yarn.lock