chenlei 1 tahun lalu
induk
melakukan
3dfff1253d
43 mengubah file dengan 1154 tambahan dan 330 penghapusan
  1. 5 1
      package.json
  2. 22 0
      postcss.config.js
  3. TEMPAT SAMPAH
      public/favicon.ico
  4. 2 2
      public/index.html
  5. TEMPAT SAMPAH
      public/videos/csbwg.mp4
  6. TEMPAT SAMPAH
      public/videos/xszc.mp4
  7. TEMPAT SAMPAH
      public/videos/ysdt.mp4
  8. 166 119
      src/App.vue
  9. 53 8
      src/api.js
  10. TEMPAT SAMPAH
      src/assets/audios/xsyc-bg.mp3
  11. TEMPAT SAMPAH
      src/assets/audios/ysdt-bg.mp3
  12. TEMPAT SAMPAH
      src/assets/images/btn0@2x-min.png
  13. TEMPAT SAMPAH
      src/assets/images/cert-min.jpg
  14. TEMPAT SAMPAH
      src/assets/images/cert-min.png
  15. TEMPAT SAMPAH
      src/assets/images/citi-of-xishan-btn-start.png
  16. TEMPAT SAMPAH
      src/assets/images/city-of-xishan-start-up-bg.jpg
  17. TEMPAT SAMPAH
      src/assets/images/city-of-xishan-start-up-bg.png
  18. TEMPAT SAMPAH
      src/assets/images/default-avatar-min.png
  19. TEMPAT SAMPAH
      src/assets/images/icon_complete@2x-min.png
  20. TEMPAT SAMPAH
      src/assets/images/icon_money.png
  21. TEMPAT SAMPAH
      src/assets/images/icon_winner@2x-min.png
  22. TEMPAT SAMPAH
      src/assets/images/login-form-bg.png
  23. TEMPAT SAMPAH
      src/assets/images/logo.png
  24. TEMPAT SAMPAH
      src/assets/images/museum/Maskgroup-min.png
  25. TEMPAT SAMPAH
      src/assets/images/museum/btn_yellow_l@2x 2-min.png
  26. TEMPAT SAMPAH
      src/assets/images/museum/cover-min.jpg
  27. TEMPAT SAMPAH
      src/assets/style/ALIBABA-PUHUITI-MEDIUM.OTF
  28. TEMPAT SAMPAH
      src/assets/style/SourceHanSansSCBold.otf
  29. 6 11
      src/components/FeedBack.vue
  30. 25 6
      src/components/IframeWrap.vue
  31. 9 5
      src/components/OtherRelicDetail.vue
  32. 92 39
      src/components/PrizeRedeem.vue
  33. 24 21
      src/components/RelicSearch.vue
  34. 1 1
      src/main.js
  35. 7 1
      src/router/index.js
  36. 9 4
      src/store/index.js
  37. 7 0
      src/utils.js
  38. 128 0
      src/views/BannerView.vue
  39. 7 6
      src/views/CityOfXishan.vue
  40. 21 6
      src/views/HomeView.vue
  41. 59 6
      src/views/MuseumView.vue
  42. 409 83
      src/views/ShopView.vue
  43. 102 11
      yarn.lock

+ 5 - 1
package.json

@@ -9,11 +9,13 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "@dage/utils": "^1.0.2",
     "@vueuse/core": "^10.4.1",
     "axios": "^1.1.3",
     "core-js": "^3.8.3",
     "dayjs": "^1.11.7",
     "element-plus": "^2.7.0",
+    "html2canvas": "^1.4.1",
     "lodash": "^4.17.21",
     "mitt": "^3.0.0",
     "v-viewer": "^3.0.11",
@@ -30,9 +32,11 @@
     "@vue/cli-plugin-router": "~5.0.0",
     "@vue/cli-plugin-vuex": "~5.0.0",
     "@vue/cli-service": "~5.0.0",
+    "autoprefixer": "^10.4.19",
     "eslint": "^7.32.0",
     "eslint-plugin-vue": "^8.0.3",
     "less": "^4.0.0",
-    "less-loader": "^8.0.0"
+    "less-loader": "^8.0.0",
+    "postcss-px-to-viewport": "^1.1.1"
   }
 }

+ 22 - 0
postcss.config.js

@@ -0,0 +1,22 @@
+module.exports = {
+  plugins: {
+    autoprefixer: {},
+    'postcss-px-to-viewport': {
+      unitToConvert: 'px', // 需要转换的单位,默认为"px"
+      viewportWidth: 970, // 设计稿的视口宽度
+      unitPrecision: 5, // 单位转换后保留的精度
+      propList: ['*'], // 能转化为vw的属性列表
+      viewportUnit: 'vh', // 希望使用的视口单位
+      fontViewportUnit: 'vh', // 字体使用的视口单位
+      selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
+      minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
+      mediaQuery: true, // 媒体查询里的单位是否需要转换单位
+      replace: true, //  是否直接更换属性值,而不添加备用属性
+      exclude: undefined, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
+      include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
+      landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
+      landscapeUnit: 'vh', // 横屏时使用的单位
+      landscapeWidth: 970 // 横屏时使用的视口宽度
+    }
+  }
+}

TEMPAT SAMPAH
public/favicon.ico


+ 2 - 2
public/index.html

@@ -4,8 +4,8 @@
     <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>
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>善云城</title>
   </head>
   <body>
     <!-- <script src="https://cdn.bootcss.com/vConsole/3.2.0/vconsole.min.js"></script> -->

TEMPAT SAMPAH
public/videos/csbwg.mp4


TEMPAT SAMPAH
public/videos/xszc.mp4


TEMPAT SAMPAH
public/videos/ysdt.mp4


+ 166 - 119
src/App.vue

@@ -1,116 +1,124 @@
 <template>
-  <div class="app-wrap">
-    <div
-      v-show="isShowTopBar"
-      class="top-bar"
-    >
-      <img
-        class="logo"
-        src="@/assets/images/logo.png"
-        alt=""
-        draggable="false"
+  <div
+    v-loading="pageLoading"
+    :class="{
+      'app-wrap': showWrap
+    }"
+  >
+    <template v-if="showWrap && route.query.hideTopBar !== '1'">
+      <div
+        v-show="isShowTopBar"
+        class="top-bar"
       >
-      <menu
-        class="tab-bar"
-      >
-        <button
-          :class="{
-            active: $route.meta.tabIdx === 1
-          }"
-          @click="$router.push({
-            name: 'HomeView'
-          })"
-        >
-          要素地图
-        </button>
-        <button
-          :class="{
-            active: $route.meta.tabIdx === 2
-          }"
-          @click="$router.push({
-            name: 'CityOfXishan'
-          })"
-        >
-          锡善云城
-        </button>
-        <button
-          :class="{
-            active: $route.meta.tabIdx === 3
-          }"
-          @click="$router.push({
-            name: 'MuseumView'
-          })"
-        >
-          慈善博物馆
-        </button>
-        <!-- <button
-          :class="{
-            active: $route.meta.tabIdx === 4
-          }"
-          @click="$router.push({
-            name: 'CloudSchool'
-          })"
-        >
-          慈善云学校
-        </button>
-        <button
-          :class="{
-            active: $route.meta.tabIdx === 5
-          }"
-          @click="$router.push({
-            name: 'SquareView'
-          })"
-        >
-          慈善广场
-        </button>
-        <button
-          :class="{
-            active: $route.meta.tabIdx === 6
-          }"
-          @click="onClickLoveForest"
+        <img
+          class="logo"
+          src="@/assets/images/logo.png"
+          alt=""
+          draggable="false"
+          @click="router.push({name: 'Banner'})"
         >
-          爱心林场
-        </button>
-        <button
-          :class="{
-            active: $route.meta.tabIdx === 7
-          }"
-          @click=" router.push({
-            name: 'CharityHall'
-          })"
+        <menu
+          class="tab-bar"
         >
-          慈善堂
-        </button> -->
-        <button
-          :class="{
-            active: $route.meta.tabIdx === 8
-          }"
-          @click="onClickFeedBack"
-        >
-          留言反馈
-        </button>
-      </menu>
-      <div class="right-button-wrap">
-        <button
-          class="shop"
-          :class="{
-            active: $route.meta.tabIdx === 9
+          <button
+            :class="{
+              active: $route.meta.tabIdx === 1
+            }"
+            @click="$router.push({
+              name: 'HomeView'
+            })"
+          >
+            要素地图
+          </button>
+          <button
+            :class="{
+              active: $route.meta.tabIdx === 2
+            }"
+            @click="$router.push({
+              name: 'CityOfXishan'
+            })"
+          >
+            锡善云城
+          </button>
+          <button
+            :class="{
+              active: $route.meta.tabIdx === 3
+            }"
+            @click="$router.push({
+              name: 'MuseumView'
+            })"
+          >
+            慈善博物馆
+          </button>
+          <!-- <button
+            :class="{
+              active: $route.meta.tabIdx === 4
+            }"
+            @click="$router.push({
+              name: 'CloudSchool'
+            })"
+          >
+            慈善云学校
+          </button>
+          <button
+            :class="{
+              active: $route.meta.tabIdx === 5
+            }"
+            @click="$router.push({
+              name: 'SquareView'
+            })"
+          >
+            慈善广场
+          </button>
+          <button
+            :class="{
+              active: $route.meta.tabIdx === 6
+            }"
+            @click="onClickLoveForest"
+          >
+            爱心林场
+          </button>
+          <button
+            :class="{
+              active: $route.meta.tabIdx === 7
+            }"
+            @click=" router.push({
+              name: 'CharityHall'
+            })"
+          >
+            慈善堂
+          </button> -->
+          <button
+            :class="{
+              active: $route.meta.tabIdx === 8
+            }"
+            @click="onClickFeedBack"
+          >
+            留言反馈
+          </button>
+        </menu>
+        <div class="right-button-wrap">
+          <button
+            class="shop"
+            :class="{
+              active: $route.meta.tabIdx === 9
 
-          }"
-          @click="onClickShop"
-        />
-        <button
-          class="hide-top-bar"
-          @click="isShowTopBar = false"
-        />
+            }"
+            @click="onClickShop"
+          />
+          <button
+            class="hide-top-bar"
+            @click="isShowTopBar = false"
+          />
+        </div>
       </div>
-    </div>
 
-    <button
-      v-show="!isShowTopBar"
-      class="show-top-bar"
-      @click="isShowTopBar = true"
-    />
+      <button
+        v-show="!isShowTopBar"
+        class="show-top-bar"
+        @click="isShowTopBar = true"
+      />
+    </template>
 
     <FeedBack
       v-if="isShowFeedBack"
@@ -124,19 +132,37 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted } from "vue"
+import { computed, ref, watch } from "vue"
 import { useRoute, useRouter } from "vue-router"
 import { useStore } from "vuex"
+import { ElMessage } from 'element-plus'
 import FeedBack from "@/components/FeedBack.vue"
-import { ElMessageBox } from 'element-plus'
 import {
   // checkLoginStatusAndProcess,
   getUserFromStorageIfNeed,
+  getShopContact
 } from '@/api.js'
+import { app } from './main'
 
+let init = false
 const route = useRoute()
 const router = useRouter()
 const store = useStore()
+const isDev = process.env.VUE_APP_CLI_MODE === 'dev'
+
+const showWrap = computed(() => route.name && route.name !== 'Banner')
+
+watch(route, () => {
+  if (!init) {
+    const cache = Number(localStorage.getItem('$isTablet'))
+    const isTablet = cache === 1 || route.query.device === 'tablet'
+    app.provide('$isTablet', isTablet)
+    if (isTablet) {
+      localStorage.setItem('$isTablet', 1)
+    }
+    init = true
+  }
+})
 
 store.commit('getPageVisitRecordFromStorage')
 // checkLoginStatusAndProcess()
@@ -153,13 +179,27 @@ function onClickFeedBack() {
   }
 }
 
-function onClickShop() {
-  if (process.env.VUE_APP_CLI_MODE === 'dev' || store.state.loginStatus === store.getters.loginStatusEnum.wxUser) {
-    router.push({
-      name: 'ShopView'
-    })
-  } else {
-    location.href = `https://open.weixin.qq.com/connect/qrconnect?appid=wx3d4f2e0cfc3b8e54&redirect_uri=https%3A%2F%2Fsit-wuxicishan.4dage.com%2F%23%2Flogin-temp&response_type=code&scope=snsapi_login&state=${encodeURIComponent('ShopView')}#wechat_redirect`
+const pageLoading = ref(false)
+async function onClickShop() {
+  try {
+    pageLoading.value = true
+    const data = await getShopContact()
+    if (data.display === 0) {
+      ElMessage({
+        message: '商城暂未开启,敬请期待',
+        type: 'warning',
+      })
+      return
+    }
+    if (isDev || store.state.loginStatus === store.getters.loginStatusEnum.wxUser) {
+      router.push({
+        name: 'ShopView'
+      })
+    } else {
+      location.href = `https://open.weixin.qq.com/connect/qrconnect?appid=wx3d4f2e0cfc3b8e54&redirect_uri=https%3A%2F%2Fsit-wuxicishan.4dage.com%2F%23%2Flogin-temp&response_type=code&scope=snsapi_login&state=${encodeURIComponent('ShopView')}#wechat_redirect`
+    }
+  } finally {
+    pageLoading.value = false
   }
 }
 </script>
@@ -191,10 +231,10 @@ html, body {
   src: url('@/assets/style/SOURCEHANSANSCN-LIGHT.OTF');
   // src: url('@/assets/style/SourceHanSansCN-Regular.otf');
 }
-// @font-face {
-//   font-family: 'Source Han Serif CN-Bold';
-//   src: url('@/assets/style/SourceHanSerifCN-Bold.otf');
-// }
+@font-face {
+  font-family: 'Source Han Sans CN-Bold';
+  src: url('@/assets/style/SourceHanSansSCBold.otf');
+}
 @font-face{
   font-family: 'Source Han Serif CN';
   src: url('@/assets/style/SourceHanSerifCN-Regular.otf');
@@ -202,6 +242,10 @@ html, body {
 // i {
 //   font-style: italic;
 // }
+@font-face{
+  font-family: 'Alibaba PuHuiTi-Bold';
+  src: url('@/assets/style/ALIBABA-PUHUITI-MEDIUM.OTF');
+}
 
 // 滚动条,只设置某一项可能导致不生效。
 &::-webkit-scrollbar { background: transparent; width: 6px; height: 0; }
@@ -274,6 +318,7 @@ html, body {
   background-size: cover;
   background-repeat: no-repeat;
   background-position: center center;
+  font-size: 14px;
   >.top-bar{
     flex: 0 0 auto;
     height: 89px;
@@ -284,6 +329,8 @@ html, body {
     justify-content: space-around;
     >img.logo{
       flex: 0 0 auto;
+      width: 131px;
+      cursor: pointer;
     }
     >menu.tab-bar{
       flex: 0 0 auto;

+ 53 - 8
src/api.js

@@ -6,7 +6,8 @@ axios.interceptors.response.use(function (response) {
   // 2xx 范围内的状态码都会触发该函数。
   if (response.data.code === 5001 || response.data.code === 5002) {
     store.commit('logoutCallback')
-    router.push({ name: 'HomeView' })
+    // router.push({ name: 'HomeView' })
+    location.href = `https://open.weixin.qq.com/connect/qrconnect?appid=wx3d4f2e0cfc3b8e54&redirect_uri=https%3A%2F%2Fsit-wuxicishan.4dage.com%2F%23%2Flogin-temp&response_type=code&scope=snsapi_login&state=${encodeURIComponent(router.currentRoute.name)}#wechat_redirect`
     return Promise.reject('登录态过期')
   }
   return response
@@ -107,7 +108,7 @@ export function submitFeedback (name, phone, content, randCode) {
     }
   }).then((res) => {
     if (res.data.code !== 0) {
-      throw ('fail')
+      throw (res.data)
     } else {
       return
     }
@@ -148,11 +149,7 @@ export function getShopContact() {
       token: store.state.token,
     },
   }).then((res) => {
-    if (res.data.data.display === 0) {
-      return res.data.data.rtf
-    } else {
-      return ''
-    }
+    return res.data.data
   })
 }
 export function getRedeemRecord() {
@@ -165,4 +162,52 @@ export function getRedeemRecord() {
   }).then((res) => {
     return res.data.data
   })
-}
+}
+export function getRankingListApi (limit = 10) {
+  return axios({
+    method: 'get',
+    url: `${process.env.VUE_APP_DEPLOY_ORIGIN}/api/cms/game/point/user/getSort`,
+    headers: {
+      token: store.state.token,
+    },
+    params: {
+      limit
+    }
+  }).then((res) => {
+    return res.data.data
+  })
+}
+export const checkRedeemApi = () => {
+  return axios({
+    method: 'get',
+    url: `${process.env.VUE_APP_DEPLOY_ORIGIN}/api/cms/game/redeem/check`,
+    headers: {
+      token: store.state.token,
+    },
+  }).then((res) => {
+    return res.data.data
+  })
+}
+export const getRedeemApi = () => {
+  return axios({
+    method: 'get',
+    url: `${process.env.VUE_APP_DEPLOY_ORIGIN}/api/cms/game/redeem/info`,
+    headers: {
+      token: store.state.token,
+    },
+  }).then((res) => {
+    return res.data.data
+  })
+}
+export function redeemApi(data) {
+  return axios({
+    method: 'post',
+    url: `${process.env.VUE_APP_DEPLOY_ORIGIN}/api/cms/game/prize/redeem`,
+    headers: {
+      token: store.state.token,
+    },
+    data
+  }).then((res) => {
+    return res.data.data
+  })
+}

TEMPAT SAMPAH
src/assets/audios/xsyc-bg.mp3


TEMPAT SAMPAH
src/assets/audios/ysdt-bg.mp3


TEMPAT SAMPAH
src/assets/images/btn0@2x-min.png


TEMPAT SAMPAH
src/assets/images/cert-min.jpg


TEMPAT SAMPAH
src/assets/images/cert-min.png


TEMPAT SAMPAH
src/assets/images/citi-of-xishan-btn-start.png


TEMPAT SAMPAH
src/assets/images/city-of-xishan-start-up-bg.jpg


TEMPAT SAMPAH
src/assets/images/city-of-xishan-start-up-bg.png


TEMPAT SAMPAH
src/assets/images/default-avatar-min.png


TEMPAT SAMPAH
src/assets/images/icon_complete@2x-min.png


TEMPAT SAMPAH
src/assets/images/icon_money.png


TEMPAT SAMPAH
src/assets/images/icon_winner@2x-min.png


TEMPAT SAMPAH
src/assets/images/login-form-bg.png


TEMPAT SAMPAH
src/assets/images/logo.png


TEMPAT SAMPAH
src/assets/images/museum/Maskgroup-min.png


TEMPAT SAMPAH
src/assets/images/museum/btn_yellow_l@2x 2-min.png


TEMPAT SAMPAH
src/assets/images/museum/cover-min.jpg


TEMPAT SAMPAH
src/assets/style/ALIBABA-PUHUITI-MEDIUM.OTF


TEMPAT SAMPAH
src/assets/style/SourceHanSansSCBold.otf


+ 6 - 11
src/components/FeedBack.vue

@@ -37,8 +37,8 @@
           </div>
           <textarea
             v-model="feedback"
-            placeholder="请输入省市区街道,50字以内"
-            maxlength="50"
+            placeholder="请输入内容,200字以内"
+            maxlength="200"
           />
         </div>
         <div class="splitter" />
@@ -88,17 +88,11 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted, nextTick, inject } from "vue"
-import { useRoute, useRouter } from "vue-router"
-import { useStore } from "vuex"
+import { ref, computed, nextTick, inject } from "vue"
 import BtnClose from "./BtnClose.vue"
 import { ElMessage } from 'element-plus'
 import { submitFeedback } from '@/api.js'
 
-const route = useRoute()
-const router = useRouter()
-const store = useStore()
-
 const $env = inject('$env')
 
 const emit = defineEmits(['close'])
@@ -127,8 +121,9 @@ function onSubmit() {
       type: 'success',
     })
     emit('close')
-  }).catch(() => {
-    ElMessage.error('网络异常,请稍后再试')
+  }).catch((err) => {
+    ElMessage.error(err)
+    emit('close')
   })
 }
 </script>

+ 25 - 6
src/components/IframeWrap.vue

@@ -8,20 +8,22 @@
     />
     <button
       v-if="props.needBackBtn"
-      class="back"
+      :class="['back', {
+        'is-close': _showClose
+      }]"
       @click="emit('back')"
     />
   </div>
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted } from "vue"
+import { ref, inject, onMounted, computed } from "vue"
 import { useRoute, useRouter } from "vue-router"
-import { useStore } from "vuex"
 
-const route = useRoute()
 const router = useRouter()
-const store = useStore()
+const route = useRoute()
+const $isTablet = inject('$isTablet')
+const $isMobile = inject('$isMobile')
 
 const emit = defineEmits(['back'])
 
@@ -31,8 +33,13 @@ onMounted(() => {
     console.log('iframe: navigateBack called!')
     router.go(-1)
   }
+  iframeRef.value.contentWindow.goToLogin = () => {
+    location.href = `https://open.weixin.qq.com/connect/qrconnect?appid=wx3d4f2e0cfc3b8e54&redirect_uri=https%3A%2F%2Fsit-wuxicishan.4dage.com%2F%23%2Flogin-temp&response_type=code&scope=snsapi_login&state=${encodeURIComponent(route.name)}#wechat_redirect`
+  }
 })
 
+const _showClose = computed(() => props.showClose && !$isMobile && !$isTablet)
+
 const props = defineProps({
   url: {
     type: String,
@@ -41,7 +48,11 @@ const props = defineProps({
   needBackBtn: {
     type: Boolean,
     default: true,
-  }
+  },
+  showClose: {
+    type: Boolean,
+    default: false,
+  },
 })
 </script>
 
@@ -69,6 +80,14 @@ const props = defineProps({
     background-size: cover;
     background-repeat: no-repeat;
     background-position: center center;
+
+    &.is-close {
+      left: unset;
+      right: 15px;
+      width: 40px;
+      height: 40px;
+      background-image: url(@/assets/images/icon_close.png);
+    }
   }
 }
 </style>

+ 9 - 5
src/components/OtherRelicDetail.vue

@@ -38,20 +38,18 @@
     <IframeWrap
       v-if="isShowIframeWrap"
       :url="iframeUrl"
+      show-close
       @back="isShowIframeWrap = false, iframeUrl = ''"
     />
   </div>
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted, inject } from "vue"
-import { useRoute, useRouter } from "vue-router"
+import { ref, inject } from "vue"
 import { useStore } from "vuex"
 import BtnClose from '@/components/BtnClose.vue'
 import IframeWrap from '@/components/IframeWrap.vue'
 
-const route = useRoute()
-const router = useRouter()
 const store = useStore()
 
 const $env = inject('$env')
@@ -67,13 +65,18 @@ const props = defineProps({
 
 const isShowIframeWrap = ref(false)
 const iframeUrl = ref('')
+const $isTablet = inject('$isTablet')
 
 function onClickSceneEntry() {
   // 运营埋点
   store.dispatch('recordPageVisitIfNeeded', {
     pageId: props.data.id,
   })
-  iframeUrl.value = props.data.link, isShowIframeWrap.value = true
+  if ($isTablet) {
+    iframeUrl.value = props.data.link, isShowIframeWrap.value = true
+  } else {
+    window.open(props.data.link)
+  }
 }
 </script>
 
@@ -91,6 +94,7 @@ function onClickSceneEntry() {
   flex-direction: column;
   justify-content: center;
   align-items: center;
+  z-index: 998;
   >h1{
     font-family: Source Han Serif CN, Source Han Serif CN;
     font-weight: 800;

+ 92 - 39
src/components/PrizeRedeem.vue

@@ -1,5 +1,8 @@
 <template>
-  <div class="prize-redeem">
+  <div
+    v-loading.fullscreen.lock="loading"
+    class="prize-redeem"
+  >
     <BtnClose
       @click="emit('close')"
     />
@@ -16,45 +19,60 @@
             v-model="name"
             aotufocus
             type="text"
-            placeholder="请输入内容,20字以内"
-            maxlength="20"
+            placeholder="请输入内容,5字以内"
+            maxlength="5"
           >
         </div>
-        <div class="row">
-          <div class="key">
-            联系方式
+        <template v-if="props.prizeData.id !== 1">
+          <div class="row">
+            <div class="key">
+              联系方式
+            </div>
+            <input
+              v-model="contact"
+              type="text"
+              placeholder="请输入内容,20字以内"
+              maxlength="20"
+            >
           </div>
-          <input
-            v-model="contact"
-            type="text"
-            placeholder="请输入内容,20字以内"
-            maxlength="20"
-          >
-        </div>
-        <div class="row">
-          <div class="key key-feedback">
-            地址及留言
+          <div class="row">
+            <div class="key key-feedback">
+              地址及留言
+            </div>
+            <textarea
+              v-model="feedback"
+              placeholder="请输入省市区街道,50字以内"
+              maxlength="50"
+            />
           </div>
-          <textarea
-            v-model="feedback"
-            placeholder="请输入省市区街道,50字以内"
-            maxlength="50"
-          />
-        </div>
+        </template>
+        <template v-else>
+          <div class="row">
+            <div class="key">
+              效果示意
+            </div>
+            <div class="cert-img-wrap">
+              <img
+                class="cert-img"
+                src="@/assets/images/cert-min.jpg"
+              >
+            </div>
+          </div>
+        </template>
 
         <div class="splitter" />
 
         <div class="row-score-current">
           <div class="key-score">
-            您的积分
+            您的爱心
           </div>
           <div class="value">
-            {{ current }}
+            {{ score }}
           </div>
         </div>
         <div class="row-score-consume">
           <div class="key-score">
-            消费
+            捐赠
           </div>
           <div class="value">
             -{{ props.prizeData.score }}
@@ -62,7 +80,7 @@
         </div>
         <div class="row-score-remain">
           <div class="key-score">
-            
+            
           </div>
           <div
             class="value"
@@ -86,7 +104,7 @@
             :disabled="!canSubmit"
             @click="onSubmit"
           >
-            提交
+            捐赠
           </button>
         </div>
       </article>
@@ -97,24 +115,27 @@
 
 <script setup>
 import { ref, computed, watch, onMounted, nextTick, inject } from "vue"
-import { useRoute, useRouter } from "vue-router"
-import { useStore } from "vuex"
 import BtnClose from "./BtnClose.vue"
+import { redeemApi } from '@/api'
+import { ElMessage } from 'element-plus'
 
-const route = useRoute()
-const router = useRouter()
-const store = useStore()
-
-const $env = inject('$env')
+const {
+  windowSizeInCssForRef,
+  windowSizeWhenDesignForRef,
+} = useSizeAdapt(1920, 972)
 
 const props = defineProps({
   prizeData: {
     type: Object,
     required: true,
+  },
+  score: {
+    type: Number,
+    required: true,
   }
 })
 
-const emit = defineEmits(['close'])
+const emit = defineEmits(['close', 'openCert', 'success'])
 
 const isShowVerifiCode = ref(true)
 const onClickVerifiCode = utils.throttle(() => {
@@ -124,21 +145,45 @@ const onClickVerifiCode = utils.throttle(() => {
   })
 }, 333)
 
+const loading = ref(false)
+
 const name = ref('')
 const contact = ref('')
 const feedback = ref('')
 
-const current = ref(424)
 const remain = computed(() => {
-  return current.value - props.prizeData.score
+  return props.score - props.prizeData.score
 })
 
 const canSubmit = computed(() => {
+  if (props.prizeData.id === 1) {
+    return name.value && (remain.value >= 0)
+  }
   return name.value && contact.value && feedback.value && (remain.value >= 0)
 })
 
-function onSubmit() {
-  emit('close')
+async function onSubmit() {
+  try {
+    loading.value = true
+    await redeemApi({
+      prizeId: props.prizeData.id,
+      score: -props.prizeData.score,
+      name: name.value,
+      phone: contact.value,
+      description: feedback.value
+    })
+    ElMessage({
+      message: '提交成功,工作人员会与您及时联系',
+      type: 'success',
+      duration: 3000
+    })
+    if (props.prizeData.id === 1) {
+      emit('openCert', name.value)
+    }
+    emit('success')
+  } finally {
+    loading.value = false
+  }
 }
 </script>
 
@@ -375,5 +420,13 @@ function onSubmit() {
       border: 1px solid rgba(255, 255, 255, 0.5);
     }
   }
+  .cert-img {
+    width: calc(412 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+    height: calc(283.5 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+
+    &-wrap {
+      width: 506px;
+    }
+  }
 }
 </style>

+ 24 - 21
src/components/RelicSearch.vue

@@ -28,12 +28,15 @@
         {{ item }}
       </button>
     </menu>
-    <ul class="relic-list">
+    <ul
+      v-if="relicList.length"
+      class="relic-list"
+    >
       <li
         v-for="item in relicList"
         :key="item.id"
         class="relic-item"
-        @click="emit('close', item)"
+        @click="emit('open', item)"
       >
         <img
           :src="`${$env.VUE_APP_DEPLOY_ORIGIN}${item.thumb}`"
@@ -58,23 +61,23 @@
         </button>
       </li>
     </ul>
+    <p
+      v-else-if="loaded"
+      style="color: white; font-size: 20px"
+    >
+      暂无内容
+    </p>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted, watchEffect, inject } from "vue"
-import { useRoute, useRouter } from "vue-router"
-import { useStore } from "vuex"
+import { ref, watchEffect, inject } from "vue"
 import BtnClose from '@/components/BtnClose.vue'
 import { getRelicList } from '@/api.js'
 
-const route = useRoute()
-const router = useRouter()
-const store = useStore()
-
 const $env = inject('$env')
 
-const emit = defineEmits(['close'])
+const emit = defineEmits(['open'])
 
 const keyword = ref('')
 
@@ -98,11 +101,13 @@ const activeAreaIdx = ref(0)
 //   desc: '上看到附近可舒服都市妇科技术都是了解对方 时刻的风景 士大夫精神地方可是对方 ',
 // },
 const relicList = ref([])
+const loaded = ref(false)
 
 watchEffect(() => {
   getRelicList(keyword.value, activeAreaIdx.value > 0 ? areaList.value[activeAreaIdx.value] : null).then((res) => {
     console.log('sdfsdf', res)
     relicList.value = res
+    loaded.value = true
   })
 })
 
@@ -125,16 +130,16 @@ watchEffect(() => {
     margin-top: 77px;
     flex: 0 0 auto;
     width: 874px;
-    height: 74px;
+    height: 70px;
     padding-left: 65px;
     padding-right: 32px;
-    background: linear-gradient( 180deg, rgba(255,255,255,0.5) 100%, rgba(255,255,255,0.2) 0%);
     box-shadow: 0px 1px 4px 0px rgba(255,255,255,0.25);
     border-radius: 36px 36px 36px 36px;
     border: 1px solid rgba(255, 255, 255, 0.5);
     display: flex;
     align-items: center;
     margin-bottom: 58px;
+    box-sizing: border-box;
     >input{
       flex: 1 0 1px;
       font-family: Source Han Sans CN, Source Han Sans CN;
@@ -176,25 +181,23 @@ watchEffect(() => {
   }
   >ul.relic-list{
     flex: 1 0 1px;
-    width: 1650px;
-    @media only screen and (max-width: 1650px) {
-      width: 1140px;
-    }
-    @media only screen and (max-width: 1150px) {
-      width: 600px;
-    }
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    width: 90%;
     overflow: auto;
     margin-bottom: 50px;
     >li.relic-item{
+      padding: 0 40px;
       height: 65px;
       display: inline-flex;
       align-items: center;
-      margin-right: 97px;
       margin-bottom: 40px;
       cursor: pointer;
       >img{
+        flex-shrink: 0;
         width: 65px;
-        height: 65PX;
+        height: 65px;
         margin-right: 26px;
         border-radius: 50%;
         border: 3px solid #FFFFFF;

+ 1 - 1
src/main.js

@@ -15,7 +15,7 @@ import 'element-plus/dist/index.css'
 console.log(`version: ${process.env.VUE_APP_VERSION}`)
 console.log(`Build time: ${process.env.VUE_APP_UPDATE_TIME}`)
 
-const app = createApp(App)
+export const app = createApp(App)
 
 // 挂载配置信息
 app.provide('$config', config)

+ 7 - 1
src/router/index.js

@@ -8,12 +8,18 @@ import SquareView from '../views/SquareView.vue'
 import LoveForest from '../views/LoveForest.vue'
 import CharityHall from '../views/CharityHall.vue'
 import ShopView from '@/views/ShopView.vue'
+import BannerView from '@/views/BannerView.vue'
 // import store from '@/store/index.js'
 
 const routes = [
   {
     path: '/',
-    redirect: '/home',
+    redirect: '/banner',
+  },
+  {
+    path: '/banner',
+    name: 'Banner',
+    component: BannerView,
   },
   {
     path: '/login-temp',

+ 9 - 4
src/store/index.js

@@ -1,20 +1,25 @@
 import { createStore } from 'vuex'
+import utils from '@/utils'
 import { reportVisitPage } from '@/api.js'
 
-const loginStatusEnum = {
+export const loginStatusEnum = {
   notLogin: 'not-login',
   visitor: 'visitor',
   wxUser: 'wxUser',
 }
 
+const token = utils.getUrlParams('token')
+const userName = utils.getUrlParams('name')
+const userId = utils.getUrlParams('userId')
+
 export default createStore({
   state: {
     haveShownStartUp: true, // 暂定不使用加载页
     loginStatus: loginStatusEnum.notLogin,
-    token: '',
+    token: token || '',
     userInfo: {
-      userName: '',
-      userId: null,
+      userName: userName || '',
+      userId: userId ? Number(userId) : null,
     },
     pageVisitRecord: {},
   },

+ 7 - 0
src/utils.js

@@ -74,5 +74,12 @@ export default {
       ret += charSet.charAt(Math.floor(Math.random() * charSet.length))
     }
     return ret
+  },
+  getUrlParams(name, search) {
+    search = search || window.location.search.substring(1) || window.location.hash.split("?")[1]
+    if (!search) return null
+    let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)")
+    let r = search.match(reg)
+    if (r != null) return decodeURI(r[2]); return null
   }
 }

+ 128 - 0
src/views/BannerView.vue

@@ -0,0 +1,128 @@
+<template>
+  <iframe
+    ref="bannerIframe"
+    frameborder="0"
+    class="banner-iframe"
+    src="https://sit-wuxicishan.4dage.com/unityPcBanner"
+  />
+
+  <div
+    v-if="showVideo && videoUrl"
+    class="banner-video"
+  >
+    <video
+      :src="`/videos/${videoUrl}`"
+      autoplay
+      :muted="$isSafari"
+      controls="false"
+      x5-playsinline="true"
+      playsinline="true"
+      webkit-playsinline="true"
+      x-webkit-airplay="true"
+      x5-video-player-type="h5-page"
+      @ended="onEnded"
+    />
+
+    <div
+      class="banner-video__skip"
+      @click="onEnded"
+    >
+      跳过
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject } from "vue"
+import { useRouter } from "vue-router"
+
+let checkedIdx = 0
+const router = useRouter()
+const videoUrl = ref('')
+const showVideo = ref(false)
+const bannerIframe = ref(null)
+const $isSafari = inject('$isSafari')
+
+const VIDEO_MAP = [
+  'ysdt.mp4',
+  'xszc.mp4',
+  'csbwg.mp4'
+]
+
+const onEnded = () => {
+  switch (checkedIdx) {
+  case 0:
+    router.push({
+      name: 'HomeView'
+    })
+    break
+  case 1:
+    router.push({
+      name: 'CityOfXishan'
+    })
+    break
+  case 2:
+    router.push({
+      name: 'MuseumView'
+    })
+    break
+  }
+
+  showVideo.value = false
+}
+
+onMounted(() => {
+  if (!bannerIframe.value) return
+
+  bannerIframe.value.contentWindow.OnClickScene = function(v) {
+    videoUrl.value = VIDEO_MAP[v]
+    showVideo.value = true
+    checkedIdx = v
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.banner-iframe {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 998;
+}
+.banner-video {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 999;
+
+  video {
+    width: inherit;
+    height: inherit;
+    object-fit: cover;
+    pointer-events: none;
+
+    &::-webkit-media-controls-enclosure {
+      display: none !important;
+    }
+  }
+  &__skip {
+    position: absolute;
+    right: 30px;
+    bottom: 30px;
+    display: flex;
+    justify-content: center;
+    font-size: 20px;
+    color: white;
+    width: 117px;
+    height: 51px;
+    letter-spacing: 4px;
+    line-height: 42px;
+    cursor: pointer;
+    background: url('@/assets/images/btn0@2x-min.png') no-repeat center / contain;
+  }
+}
+</style>

+ 7 - 6
src/views/CityOfXishan.vue

@@ -26,17 +26,18 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted, inject } from "vue"
-import { useRoute, useRouter } from "vue-router"
+import { ref, computed, inject } from "vue"
+import { useRoute } from "vue-router"
 import { useStore } from "vuex"
 import IframeWrap from '@/components/IframeWrap.vue'
 import LoginForm from '@/components/LoginForm.vue'
+import { loginStatusEnum } from '@/store'
 
 const route = useRoute()
-const router = useRouter()
 const store = useStore()
 
 const $env = inject('$env')
+const $isTablet = inject('$isTablet')
 
 // 运营埋点
 store.dispatch('recordPageVisitIfNeeded', {
@@ -44,14 +45,14 @@ store.dispatch('recordPageVisitIfNeeded', {
 })
 
 const url = computed(() => {
-  let temp = `${$env.VUE_APP_DEPLOY_ORIGIN}/unityForPc/index.html?platform=h5&name=${store.state.userInfo.userName}`
+  let temp = `${$env.VUE_APP_DEPLOY_ORIGIN}/${$isTablet ? 'unityForPad' : 'unityForPc'}/index.html?platform=${$isTablet ? 'pad' : 'h5'}&name=${encodeURIComponent(store.state.userInfo.userName)}`
   if (route.query.scene !== undefined) {
     temp += `&scene=${route.query.scene}`
   }
   return temp
 })
 
-const isShowStartUp = ref(true)
+const isShowStartUp = ref(store.state.loginStatus === loginStatusEnum.notLogin)
 const isShowStartBtn = ref(false)
 const isShowLoginForm = ref(false)
 
@@ -85,7 +86,7 @@ function onClickStartBtn() {
     top: 0;
     width: 100%;
     height: 100%;
-    background-image: url(@/assets/images/city-of-xishan-start-up-bg.jpg);
+    background-image: url(@/assets/images/city-of-xishan-start-up-bg.png);
     background-size: cover;
     background-repeat: no-repeat;
     background-position: center center;

+ 21 - 6
src/views/HomeView.vue

@@ -186,29 +186,39 @@
 
     <RelicSearch
       v-if="isShowRelicSearch"
-      @close="onSearchViewClose"
+      @open="onSearchViewClose"
+      @close="isShowRelicSearch = false"
     />
 
     <IframeWrap
       v-if="isShowIframeWrap"
       class="iframe-wrap"
       :url="iframeUrl"
+      show-close
       @back="isShowIframeWrap = false, iframeUrl = ''"
     />
+
+    <audio
+      :src="bgAudio"
+      loop
+      autoplay
+      preload="auto"
+      style="display: none;"
+    />
   </div>
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted, watchEffect, inject } from "vue"
-import { useRoute, useRouter } from "vue-router"
+import { ref, watch, watchEffect, inject } from "vue"
+import { useRouter } from "vue-router"
 import { useStore } from "vuex"
 import { useElementSize } from '@vueuse/core'
 import OtherRelicDetail from '@/components/OtherRelicDetail.vue'
 import RelicSearch from '@/components/RelicSearch.vue'
 import IframeWrap from '@/components/IframeWrap.vue'
 import { getRelicList } from '@/api.js'
+import bgAudio from '@/assets/audios/ysdt-bg.mp3'
 
-const route = useRoute()
 const router = useRouter()
 const store = useStore()
 
@@ -616,12 +626,17 @@ watch(activeHotRelicIdx, (v) => {
 }, {
   immediate: true,
 })
+const $isTablet = inject('$isTablet')
 function onClickHotRelicSceneEntry() {
   // 运营埋点
   store.dispatch('recordPageVisitIfNeeded', {
     pageId: activeHotRelicDetail.value.id,
   })
-  iframeUrl.value = activeHotRelicDetail.value?.link, isShowIframeWrap.value = true
+  if ($isTablet) {
+    iframeUrl.value = activeHotRelicDetail.value?.link, isShowIframeWrap.value = true
+  } else {
+    window.open(activeHotRelicDetail.value?.link)
+  }
 }
 
 const isShowAreaList = ref(true)
@@ -651,7 +666,7 @@ function onClickArea(idx) {
 
 const isShowRelicSearch = ref(false)
 function onSearchViewClose(relicData) {
-  isShowRelicSearch.value = false
+  // isShowRelicSearch.value = false
   if (relicData) {
     activeOtherRelicData.value = relicData
     isShowOtherRelicDetail.value = true

+ 59 - 6
src/views/MuseumView.vue

@@ -1,21 +1,38 @@
 <template>
   <IframeWrap
+    v-if="showIframe"
     class="iframe-wrap"
-    url="https://houseoss.4dkankan.com/project/wuxicishanbwg/index.html?m=SG-igv7wQAyyyG_01#/"
+    url="https://sit-locbigsecen.4dage.com/wxScene/index.html?m=wxcs_SG-igv7wQAyyyG"
     :need-back-btn="false"
   />
-  <!-- @back="onClickBack" -->
+
+  <div
+    v-else
+    class="museum-start"
+  >
+    <div class="museum-start__top">
+      <img
+        src="@/assets/images/museum/Maskgroup-min.png"
+      >
+      <p>内容建设中 敬请期待...</p>
+    </div>
+
+    <!-- <div
+      class="museum-start__btn"
+      @click="showIframe = true"
+    >
+      开始漫游
+    </div> -->
+  </div>
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted } from "vue"
-import { useRoute, useRouter } from "vue-router"
 import { useStore } from "vuex"
 import IframeWrap from '@/components/IframeWrap.vue'
+import { ref } from "vue"
 
-const route = useRoute()
-const router = useRouter()
 const store = useStore()
+const showIframe = ref(false)
 
 // 运营埋点
 store.dispatch('recordPageVisitIfNeeded', {
@@ -24,6 +41,42 @@ store.dispatch('recordPageVisitIfNeeded', {
 </script>
 
 <style lang="less" scoped>
+.museum-start {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background: url('@/assets/images/museum/cover-min.jpg') no-repeat center bottom / cover;
+
+  &__top {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    color: white;
+    text-shadow: 0px 4px 12px rgba(0,0,0,0.42);
+    font-size: 26px;
+    letter-spacing: 3px;
+  }
+  &__btn {
+    position: absolute;
+    left: 50%;
+    bottom: 120px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding-bottom: 14px;
+    width: 191px;
+    height: 55px;
+    color: white;
+    font-size: 22px;
+    letter-spacing: 3px;
+    background: url('@/assets/images/museum/btn_yellow_l@2x 2-min.png') no-repeat center / contain;
+    cursor: pointer;
+    box-sizing: border-box;
+    transform: translateX(-50%);
+  }
+}
 .iframe-wrap{
   position: absolute;
   left: 0;

+ 409 - 83
src/views/ShopView.vue

@@ -1,5 +1,7 @@
 <template>
-  <div class="shop-view">
+  <div
+    class="shop-view"
+  >
     <menu class="tab-bar">
       <button
         :class="{
@@ -7,7 +9,15 @@
         }"
         @click="tabIdx = 0"
       >
-        爱心币兑换
+        爱心兑换
+      </button>
+      <button
+        :class="{
+          active: tabIdx === 2
+        }"
+        @click="tabIdx = 2"
+      >
+        排行榜
       </button>
       <button
         :class="{
@@ -15,73 +25,88 @@
         }"
         @click="tabIdx = 1"
       >
-        兑换记录
+        爱心记录
       </button>
     </menu>
 
-    <button
-      v-show="tabIdx === 0"
-      class="prev-page"
-      :class="{
-        hide: pageNumber === 0
-      }"
-      @click="onClickPrevPage"
-    />
-    <ul
-      v-show="tabIdx === 0"
-      class="prize-list"
-    >
-      <li
-        v-for="prizeItem in prizeList"
-        :key="prizeItem.id"
-        class="prize"
+    <template v-if="tabIdx === 0">
+      <button
+        class="prev-page"
         :class="{
-          disabled: prizeItem.stock === 0,
+          hide: pageNumber === 0
         }"
-        @click="onClickPrizeItem(prizeItem)"
+        @click="onClickPrevPage"
+      />
+      <ul
+        v-loading.fullscreen.lock="pageLoading"
+        class="prize-list"
       >
-        <img
-          class="thumb"
-          :src="`${$env.VUE_APP_DEPLOY_ORIGIN}${prizeItem.thumb}`"
-          alt=""
-          draggable="false"
-        >
-        <div class="title">
-          {{ prizeItem.name }}
-        </div>
-        <div
-          class="remaining"
+        <li
+          v-for="prizeItem in prizeList"
+          :key="prizeItem.id"
+          class="prize"
+          :class="{
+            disabled: prizeItem.stock === 0,
+          }"
+          @click="onClickPrizeItem(prizeItem)"
         >
-          剩余:{{ prizeItem.stock }}
-        </div>
-        <div class="price">
-          <span class="number">{{ prizeItem.score }}</span>
-          <span class="unit">爱心币</span>
-        </div>
-        <img
-          v-show="prizeItem.stock > 0"
-          class="icon-enabled"
-          src="@/assets/images/icon-gift.png"
-          alt=""
-          draggable="false"
+          <img
+            class="thumb"
+            :src="`${$env.VUE_APP_DEPLOY_ORIGIN}${prizeItem.thumb}`"
+            alt=""
+            draggable="false"
+          >
+          <div class="prize-inner">
+            <div class="title">
+              {{ prizeItem.name }}
+            </div>
+            <div
+              class="remaining"
+            >
+              <span v-if="prizeItem.id !== 1">剩余:{{ prizeItem.stock }}</span>
+            </div>
+            <div class="price">
+              <span class="number">{{ prizeItem.score }}</span>
+              <span class="unit">爱心</span>
+            </div>
+            <img
+              v-show="prizeItem.stock > 0 || (prizeItem.id === 1 && !isRedeemed)"
+              class="icon-enabled"
+              src="@/assets/images/icon-gift.png"
+              alt=""
+              draggable="false"
+            >
+            <img
+              v-if="prizeItem.id === 1 && isRedeemed"
+              class="icon-enabled redeemed"
+              src="@/assets/images/icon_complete@2x-min.png"
+              alt=""
+              draggable="false"
+            >
+            <!-- <img
+              v-show="prizeItem.isEnabled && prizeItem.stock === 0"
+              class="icon-no-stock"
+              src="@/assets/images/no-stock.png"
+              alt=""
+              draggable="false"
+            > -->
+          </div>
+        </li>
+
+        <p
+          class="product-tip"
         >
-        <!-- <img
-          v-show="prizeItem.isEnabled && prizeItem.stock === 0"
-          class="icon-no-stock"
-          src="@/assets/images/no-stock.png"
-          alt=""
-          draggable="false"
-        > -->
-      </li>
-    </ul>
-    <button
-      v-show="tabIdx === 0"
-      class="next-page"
-      :class="{
-        hide: !haveNextPage
-      }"
-      @click="onClickNextPage"
-    />
+          更多礼品敬请期待
+        </p>
+      </ul>
+      <button
+        class="next-page"
+        :class="{
+          hide: !haveNextPage
+        }"
+        @click="onClickNextPage"
+      />
+    </template>
 
     <div
       v-show="tabIdx === 1"
@@ -97,7 +122,7 @@
         <div class="row-header">
           <div>时间</div>
           <div>类型</div>
-          <div>爱心</div>
+          <div>爱心</div>
           <div>说明</div>
         </div>
         <div class="splitter" />
@@ -114,7 +139,7 @@
               {{ item.type }}
             </div>
             <div>
-              {{ item.score > 0 ? '+' : item.score < 0 ? '-' : '' }}{{ item.score }}
+              {{ item.score > 0 ? '+' : item.score < 0 ? '' : '' }}{{ item.score }}
             </div>
             <div>
               {{ item.description || `(空)` }}
@@ -124,6 +149,47 @@
       </div>
     </div>
 
+    <div
+      v-show="tabIdx === 2"
+      class="ranking-content"
+    >
+      <div
+        v-loading="rankingLoading"
+        class="table"
+      >
+        <div class="row-header">
+          <div>排名</div>
+          <div>用户名</div>
+          <div>爱心</div>
+        </div>
+        <div class="splitter" />
+        <div class="table-content">
+          <div
+            v-for="(item, idx) in rankingList"
+            :key="item.id"
+            :class="['row-data', {
+              'is-me': store.state.userInfo.userId === item.id
+            }]"
+          >
+            <div :class="['ranking-sort', idx === 0 && 'is-first']">
+              <span>{{ idx + 1 }}</span>
+            </div>
+            <div class="ranking-name">
+              <img
+                class="ranking-name__avatar"
+                fit="cover"
+                src="@/assets/images/default-avatar-min.png"
+              >
+              <span>{{ item.nickName.slice(0, 1) }}***</span>
+            </div>
+            <div>
+              {{ item.pcs }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
     <div class="coin-number">
       <img
         class="icon"
@@ -137,14 +203,24 @@
     <PrizeRedeem
       v-if="isShowRedeem"
       :prize-data="redeemPrizeData"
+      :score="myScore"
+      @open-cert="openCertImg"
       @close="redeemPrizeData = {}, isShowRedeem = false"
+      @success="handlePrizeSuccess"
     />
   </div>
+
+  <div
+    ref="certCanvas"
+    class="cert-canvas"
+  >
+    <p>{{ certInfo.certName }}</p>
+    <p>{{ certInfo.createTime }}</p>
+  </div>
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted, inject } from "vue"
-import { useRoute, useRouter } from "vue-router"
+import { ref, computed, watch, onMounted, inject, nextTick } from "vue"
 import { useStore } from "vuex"
 import PrizeRedeem from '@/components/PrizeRedeem.vue'
 import {
@@ -152,12 +228,19 @@ import {
   getShopContact,
   getPrizeList,
   getRedeemRecord,
+  getRankingListApi,
+  checkRedeemApi,
+  getRedeemApi
 } from '@/api.js'
+import html2canvas from 'html2canvas'
+import { formatDate } from '@dage/utils'
+import { ElMessage } from 'element-plus'
+import { useRouter } from 'vue-router'
 
-const route = useRoute()
-const router = useRouter()
 const store = useStore()
+const router = useRouter()
 
+const pageLoading = ref(false)
 const $env = inject('$env')
 
 const {
@@ -174,9 +257,34 @@ const tabIdx = ref(0)
 
 const redeemRecordContact = ref('')
 getShopContact().then((res) => {
-  redeemRecordContact.value = res
+  if (res.display === 0) {
+    ElMessage({
+      message: '商城暂未开启,敬请期待',
+      type: 'warning',
+    })
+    router.replace({
+      name: 'HomeView'
+    })
+    return
+  }
+  redeemRecordContact.value = res.rtf
 })
 
+const handlePrizeSuccess = async() => {
+  try {
+    pageLoading.value = true
+    await checkRedeem()
+    const scoreRes = await getMyScore()
+    myScore.value = scoreRes
+    const res = await getPrizeList(pageNumber.value, 8)
+    total.value = res.total
+    prizeList.value = res.records
+  } finally {
+    pageLoading.value = false
+  }
+  isShowRedeem.value = false
+}
+
 /**
  * 一页页的商品
  */
@@ -208,6 +316,16 @@ watch(pageNumber, (v) => {
 const isShowRedeem = ref(false)
 const redeemPrizeData = ref({})
 function onClickPrizeItem(prizeItem) {
+  if (prizeItem.id === 1 && isRedeemed.value) {
+    openCertImg()
+    return
+  }
+
+  if (prizeItem.score > myScore.value) {
+    ElMessage.warning('积分不足,无法兑换')
+    return
+  }
+
   redeemPrizeData.value = prizeItem
   isShowRedeem.value = true
 }
@@ -217,6 +335,80 @@ getRedeemRecord().then((res) => {
   redeemRecord.value = res
 })
 
+const rankingList = ref([])
+const rankingLoading = ref(false)
+const getRankingList = async() => {
+  try {
+    rankingLoading.value = true
+    const data = await getRankingListApi()
+    rankingList.value = data
+  } finally {
+    rankingLoading.value = false
+  }
+}
+
+watch(tabIdx, val => {
+  if (val === 2) {
+    getRankingList()
+  }
+})
+
+onMounted(() => {
+  checkRedeem()
+})
+
+const isRedeemed = ref(false)
+const checkRedeem = async() => {
+  const data = await checkRedeemApi()
+  isRedeemed.value = data
+}
+
+const certCanvas = ref()
+const $isSafari = inject('$isSafari')
+const openCertImg = async(certName) => {
+  try {
+    pageLoading.value = true
+    let winRef
+    if ($isSafari) {
+      winRef = window.open('', '_blank')
+    }
+    if (isRedeemed.value) {
+      await getRedeem()
+    } else {
+      certInfo.value = {
+        certName,
+        createTime: formatDate(new Date(), "YYYY年MM月DD日")
+      }
+    }
+    await nextTick(async() => {
+      const canvas = await html2canvas(certCanvas.value, {
+        width: certCanvas.value.offsetWidth,
+        scale: 2
+      })
+      canvas.toBlob((blob) => {
+        if (blob) {
+          if ($isSafari && winRef) {
+            winRef.location.href = URL.createObjectURL(blob)
+          } else {
+            window.open(URL.createObjectURL(blob))
+          }
+        }
+      })
+    })
+  } finally {
+    pageLoading.value = false
+  }
+}
+
+const certInfo = ref({
+  certName: '',
+  createTime: ''
+})
+const getRedeem = async() => {
+  const data = await getRedeemApi()
+  data.createTime = formatDate(data.createTime, "YYYY年MM月DD日")
+  certInfo.value = data
+}
 </script>
 
 <style lang="less" scoped>
@@ -236,6 +428,7 @@ getRedeemRecord().then((res) => {
     align-items: center;
     width: calc(300 / 1920 * 100%);
     >button{
+      margin: 28px 0;
       font-family: Source Han Sans CN, Source Han Sans CN;
       font-weight: 400;
       font-size: 28px;
@@ -259,9 +452,6 @@ getRedeemRecord().then((res) => {
         }
       }
     }
-    >button:first-of-type{
-      margin-bottom: 38px;
-    }
   }
   >button.prev-page{
     width: 58px;
@@ -276,11 +466,19 @@ getRedeemRecord().then((res) => {
       opacity: 0;
     }
   }
-  >ul.prize-list{
+  ul.prize-list{
+    position: relative;
     flex: 0 0 auto;
     width: calc(1400 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
     height: calc((393 * 2 + 25) / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
-    >li.prize{
+    .product-tip {
+      position: absolute;
+      left: 50%;
+      bottom: -20px;
+      transform: translateX(-50%);
+      color: #666;
+    }
+    li.prize{
       display: inline-block;
       width: calc(322 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
       height: calc(393 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
@@ -298,10 +496,11 @@ getRedeemRecord().then((res) => {
         object-fit: cover;
         border-radius: calc(4 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
       }
-      >.title{
+      .prize-inner {
+        padding: 0 calc(22 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+      }
+      .title{
         margin-top: calc(13 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
-        margin-left: calc(22 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
-        margin-right: calc(22 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         font-size: calc(28 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         font-family: Source Han Sans SC, Source Han Sans SC;
         color: #424A4A;
@@ -311,10 +510,9 @@ getRedeemRecord().then((res) => {
         white-space: pre;
         text-overflow: ellipsis;
       }
-      >.remaining{
+      .remaining{
+        min-height: calc(20 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         margin-top: calc(5 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
-        margin-left: calc(22 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
-        margin-right: calc(22 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         font-size: calc(16 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         font-family: Source Han Sans SC, Source Han Sans SC;
         font-weight: 400;
@@ -324,10 +522,9 @@ getRedeemRecord().then((res) => {
         white-space: pre;
         text-overflow: ellipsis;
       }
-      >.price{
+      .price{
         margin-top: calc(10 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         >.number{
-          margin-left: calc(22 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
           font-family: Source Han Sans CN, Source Han Sans CN;
           font-weight: bold;
           font-size: calc(28 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
@@ -343,12 +540,17 @@ getRedeemRecord().then((res) => {
           margin-left: 0.5em;
         }
       }
-      >img.icon-enabled{
+      img.icon-enabled{
         position: absolute;
         right: calc(29 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         bottom: calc(20 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         width: calc(57 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
         height: calc(57 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+
+        &.redeemed {
+          width: calc(80 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+          height: calc(80 / v-bind('windowSizeWhenDesignForRef') * v-bind('windowSizeInCssForRef'));
+        }
       }
     }
     >article.prize-item.disabled{
@@ -367,6 +569,103 @@ getRedeemRecord().then((res) => {
       opacity: 0;
     }
   }
+  .ranking-sort {
+    position: relative;
+    font-family: 'Alibaba PuHuiTi-Bold' !important;
+
+    &.is-first {
+      color: #70581A !important;
+
+      &::after {
+        content: '';
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        width: 36px;
+        height: 36px;
+        background: url('@/assets/images/icon_winner@2x-min.png') no-repeat center / contain;
+        transform: translate(-50%, -50%);
+      }
+    }
+    span {
+      position: relative;
+      z-index: 1;
+    }
+  }
+  .ranking-name {
+    display: flex !important;
+    align-items: center;
+    justify-content: center;
+    gap: 17px;
+
+    &__avatar {
+      width: 36px;
+      height: 36px;
+      border: 2px solid white;
+      border-radius: 50%;
+    }
+  }
+  .ranking-content{
+    flex: 1 0 1px;
+    height: 100%;
+    margin: 25px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .table{
+      margin-top: 14px;
+      flex: 1 0 1px;
+      width: 100%;
+      margin-bottom: 43px;
+      background: linear-gradient( 180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 100%);
+      box-shadow: 15px 15px 38px 0px rgba(255,255,255,0.5);
+      border-radius: 8px 8px 8px 8px;
+      display: flex;
+      flex-direction: column;
+      >.row-header{
+        flex: 0 0 auto;
+        height: 72px;
+        display: flex;
+        align-items: center;
+        margin-right: 16px;
+        >div{
+          display: inline-block;
+          width: 33.3333%;
+          text-align: center;
+          font-family: Source Han Sans CN, Source Han Sans CN;
+          font-weight: bold;
+          font-size: 20px;
+          color: #424A4A;
+          line-height: 23px;
+        }
+      }
+      .table-content{
+        flex: 1 0 1px;
+        overflow: auto;
+        margin-right: 10px;
+        >.row-data{
+          height: 68px;
+          display: flex;
+          align-items: center;
+          &.is-me {
+            background: linear-gradient( 90deg, rgba(88,148,152,0.1) 0%, #589498 50%, rgba(88,148,152,0.1) 100%);
+          }
+          >div{
+            display: inline-block;
+            width: 33.3333%;
+            text-align: center;
+            font-family: Source Han Sans CN, Source Han Sans CN;
+            font-weight: 400;
+            font-size: 20px;
+            color: #424A4A;
+            line-height: 23px;
+            opacity: 0.8;
+          }
+        }
+      }
+    }
+  }
   >.redeem-record-content{
     flex: 1 0 1px;
     height: 100%;
@@ -450,9 +749,10 @@ getRedeemRecord().then((res) => {
     justify-content: center;
     align-items: center;
     >img.icon{
+      position: relative;
+      top: -15px;
       width: 59px;
       height: 59px;
-      margin-bottom: 15px;
       margin-right: 20px;
     }
     >.value{
@@ -466,4 +766,30 @@ getRedeemRecord().then((res) => {
     }
   }
 }
+
+.cert-canvas {
+  position: absolute;
+  top: calc(-200% - 1920px);
+  left: calc(-200% - 2880px);
+  width: 2880px;
+  height: 1920px;
+  background: url('@/assets/images/cert-min.png') no-repeat center / contain;
+
+  p:first-child {
+    position: absolute;
+    top: calc(50% - 16px);
+    left: 50%;
+    color: #CFC49E;
+    font-size: 140px;
+    letter-spacing: 10px;
+    font-family: 'Source Han Sans CN-Bold';
+    transform: translate(-50%, -50%);
+  }
+  p:last-child {
+    position: absolute;
+    left: 765px;
+    bottom: 310px;
+    font-size: 40px;
+  }
+}
 </style>

+ 102 - 11
yarn.lock

@@ -1027,6 +1027,21 @@
   resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31"
   integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
 
+"@dage/events@^1.0.1":
+  version "1.0.1"
+  resolved "http://192.168.20.245:4873/@dage/events/-/events-1.0.1.tgz#eafd540fb9f97c847a2ff273d4d1791e524aebf0"
+  integrity sha512-VHNVJbY5gAMvqur7pOmxZ8W9l4LRnwK/OqMIuAt4VLXLkldUTyyfJmWRkPpCdHDWNbn7bATgAA/+ziV01rozDA==
+
+"@dage/utils@^1.0.2":
+  version "1.0.2"
+  resolved "http://192.168.20.245:4873/@dage/utils/-/utils-1.0.2.tgz#c3dfbe75a0dcc87c5fff1f15d068a86fd18f249b"
+  integrity sha512-txmTlVDYn9wwq1Hfcn0r893c53u5faftqyUjgw95u2hzLHeELI1FM7OxcfOUcYLMOMx4zY1T54M6mpQiCr0VrQ==
+  dependencies:
+    "@dage/events" "^1.0.1"
+    dayjs "^1.11.9"
+    js-base64 "^3.7.5"
+    query-string "^8.1.0"
+
 "@discoveryjs/json-ext@0.5.7":
   version "0.5.7"
   resolved "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
@@ -2157,7 +2172,7 @@ at-least-node@^1.0.0:
   resolved "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
-autoprefixer@^10.2.4:
+autoprefixer@^10.2.4, autoprefixer@^10.4.19:
   version "10.4.19"
   resolved "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f"
   integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==
@@ -2224,6 +2239,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-arraybuffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
+  integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
+
 base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -2717,6 +2737,13 @@ css-declaration-sorter@^6.3.1:
   resolved "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71"
   integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==
 
+css-line-break@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
+  integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
+  dependencies:
+    utrie "^1.0.2"
+
 css-loader@^6.5.0:
   version "6.10.0"
   resolved "https://registry.npmmirror.com/css-loader/-/css-loader-6.10.0.tgz#7c172b270ec7b833951b52c348861206b184a4b7"
@@ -2838,6 +2865,11 @@ dayjs@^1.11.3, dayjs@^1.11.7:
   resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
   integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
 
+dayjs@^1.11.9:
+  version "1.11.11"
+  resolved "http://192.168.20.245:4873/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
+  integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
+
 debounce@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmmirror.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
@@ -2864,6 +2896,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
   dependencies:
     ms "2.1.2"
 
+decode-uri-component@^0.4.1:
+  version "0.4.1"
+  resolved "http://192.168.20.245:4873/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5"
+  integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==
+
 deep-is@^0.1.3:
   version "0.1.4"
   resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@@ -3489,6 +3526,11 @@ fill-range@^7.0.1:
   dependencies:
     to-regex-range "^5.0.1"
 
+filter-obj@^5.1.0:
+  version "5.1.0"
+  resolved "http://192.168.20.245:4873/filter-obj/-/filter-obj-5.1.0.tgz#5bd89676000a713d7db2e197f660274428e524ed"
+  integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==
+
 finalhandler@1.2.0:
   version "1.2.0"
   resolved "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
@@ -3832,6 +3874,14 @@ html-webpack-plugin@^5.1.0:
     pretty-error "^4.0.0"
     tapable "^2.0.0"
 
+html2canvas@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
+  integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
+  dependencies:
+    css-line-break "^2.1.0"
+    text-segmentation "^1.0.3"
+
 htmlparser2@^6.1.0:
   version "6.1.0"
   resolved "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
@@ -4141,6 +4191,11 @@ joi@^17.4.0:
     "@sideway/formula" "^3.0.1"
     "@sideway/pinpoint" "^2.0.0"
 
+js-base64@^3.7.5:
+  version "3.7.7"
+  resolved "http://192.168.20.245:4873/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79"
+  integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
+
 js-message@1.0.7:
   version "1.0.7"
   resolved "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz#fbddd053c7a47021871bb8b2c95397cc17c20e47"
@@ -4736,7 +4791,7 @@ nth-check@^2.0.1:
   dependencies:
     boolbase "^1.0.0"
 
-object-assign@^4.0.1:
+object-assign@>=4.0.1, object-assign@^4.0.1:
   version "4.1.1"
   resolved "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
@@ -5207,6 +5262,14 @@ postcss-ordered-values@^5.1.3:
     cssnano-utils "^3.1.0"
     postcss-value-parser "^4.2.0"
 
+postcss-px-to-viewport@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/postcss-px-to-viewport/-/postcss-px-to-viewport-1.1.1.tgz#a25ca410b553c9892cc8b525cc710da47bf1aa55"
+  integrity sha512-2x9oGnBms+e0cYtBJOZdlwrFg/mLR4P1g2IFu7jYKvnqnH/HLhoKyareW2Q/x4sg0BgklHlP1qeWo2oCyPm8FQ==
+  dependencies:
+    object-assign ">=4.0.1"
+    postcss ">=5.0.2"
+
 postcss-reduce-initial@^5.1.2:
   version "5.1.2"
   resolved "https://registry.npmmirror.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6"
@@ -5250,15 +5313,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
   resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
 
-postcss@^7.0.36:
-  version "7.0.39"
-  resolved "https://registry.npmmirror.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
-  integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
-  dependencies:
-    picocolors "^0.2.1"
-    source-map "^0.6.1"
-
-postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.35:
+postcss@>=5.0.2, postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.35:
   version "8.4.38"
   resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
   integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
@@ -5267,6 +5322,14 @@ postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.35:
     picocolors "^1.0.0"
     source-map-js "^1.2.0"
 
+postcss@^7.0.36:
+  version "7.0.39"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
+  integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
+  dependencies:
+    picocolors "^0.2.1"
+    source-map "^0.6.1"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -5347,6 +5410,15 @@ qs@6.11.0:
   dependencies:
     side-channel "^1.0.4"
 
+query-string@^8.1.0:
+  version "8.2.0"
+  resolved "http://192.168.20.245:4873/query-string/-/query-string-8.2.0.tgz#f0b0ef6caa85f525dbdb745a67d3f8c08d71cc6b"
+  integrity sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g==
+  dependencies:
+    decode-uri-component "^0.4.1"
+    filter-obj "^5.1.0"
+    split-on-first "^3.0.0"
+
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -5854,6 +5926,11 @@ spdy@^4.0.2:
     select-hose "^2.0.0"
     spdy-transport "^3.0.0"
 
+split-on-first@^3.0.0:
+  version "3.0.0"
+  resolved "http://192.168.20.245:4873/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7"
+  integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==
+
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -6040,6 +6117,13 @@ terser@^5.10.0, terser@^5.26.0:
     commander "^2.20.0"
     source-map-support "~0.5.20"
 
+text-segmentation@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
+  integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
+  dependencies:
+    utrie "^1.0.2"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -6205,6 +6289,13 @@ utils-merge@1.0.1:
   resolved "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
   integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
 
+utrie@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
+  integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
+  dependencies:
+    base64-arraybuffer "^1.0.2"
+
 uuid@^8.3.2:
   version "8.3.2"
   resolved "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"