lvkun996 пре 2 година
родитељ
комит
40caeadd4c

+ 4 - 0
README.md

@@ -8,6 +8,10 @@
 
 ​	mp-weixin
 
+## luojigou-board
+
+
+
 ## 参数
 
 	### gameview:

+ 31 - 4
package-lock.json

@@ -294,6 +294,14 @@
         "@babel/plugin-syntax-typescript": "^7.20.0"
       }
     },
+    "@babel/runtime": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.21.0.tgz",
+      "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
+      "requires": {
+        "regenerator-runtime": "^0.13.11"
+      }
+    },
     "@babel/standalone": {
       "version": "7.21.3",
       "resolved": "https://registry.npmmirror.com/@babel/standalone/-/standalone-7.21.3.tgz",
@@ -1710,11 +1718,15 @@
         "is-what": "^3.14.1"
       }
     },
+    "copy-text-to-clipboard": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.1.0.tgz",
+      "integrity": "sha512-PFM6BnjLnOON/lB3ta/Jg7Ywsv+l9kQGD4TWDCSlRBGmqnnTM5MrDkhAFgw+8HZt0wW6Q2BBE4cmy9sq+s9Qng=="
+    },
     "core-js": {
       "version": "3.29.1",
       "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.29.1.tgz",
-      "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==",
-      "dev": true
+      "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw=="
     },
     "cross-env": {
       "version": "7.0.3",
@@ -2549,6 +2561,11 @@
       "integrity": "sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==",
       "dev": true
     },
+    "mutation-observer": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/mutation-observer/-/mutation-observer-1.0.3.tgz",
+      "integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
+    },
     "nanoid": {
       "version": "3.3.6",
       "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz",
@@ -2866,8 +2883,7 @@
     "regenerator-runtime": {
       "version": "0.13.11",
       "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
-      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
-      "dev": true
+      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
     },
     "resolve": {
       "version": "1.22.1",
@@ -3217,6 +3233,17 @@
       "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
       "dev": true
     },
+    "vconsole": {
+      "version": "3.15.0",
+      "resolved": "https://registry.npmmirror.com/vconsole/-/vconsole-3.15.0.tgz",
+      "integrity": "sha512-8hq7wabPcRucSWQyN7/1tthMawP9JPvM95zgtMHpPknMMMCKj+abpoK7P7oKK4B0qw58C24Mdvo9+raUdpHyVQ==",
+      "requires": {
+        "@babel/runtime": "^7.17.2",
+        "copy-text-to-clipboard": "^3.0.1",
+        "core-js": "^3.11.0",
+        "mutation-observer": "^1.0.3"
+      }
+    },
     "vite": {
       "version": "4.0.4",
       "resolved": "https://registry.npmmirror.com/vite/-/vite-4.0.4.tgz",

+ 1 - 0
package.json

@@ -52,6 +52,7 @@
     "@rive-app/canvas": "^1.0.102",
     "less-loader": "^6.2.0",
     "pinia": "^2.0.33",
+    "vconsole": "^3.15.0",
     "vue": "^3.2.45",
     "vue-hooks-plus": "^1.6.0",
     "vue-i18n": "^9.1.9"

+ 16 - 0
src/api/card.ts

@@ -16,3 +16,19 @@ export const getCardDetailById = (id: string, mode: CardModeEnum) => {
 }
 
 
+
+
+/**
+ * 提交学习计划
+ * @param params 
+ * @returns 
+ */
+export const submitlearnPortAns = (params: any) => {
+  return request<API.Card>({
+    url: `/app/game-course/data/task2`,
+    method: 'POST',
+    params
+  })
+}
+
+

BIN
src/assets/coin-ani.png


BIN
src/assets/success-flag.png


+ 7 - 2
src/components/luojigou-board/README.md

@@ -1,7 +1,7 @@
 
 # 题库程序
 
-
+## 云赛拖拽逻辑
 存在五种拖拽情况
 1. 答案区为空, 按钮直接放在答案区
 2. 答案区存在按钮 , 两个按钮互换位置
@@ -9,4 +9,9 @@
 4. 没有放在答案区, 按钮回到初始位置
 5. 两个按钮都在答案区, 直接进行按钮之间的交换
 6. 将在答案区的按钮直接拖动到另外一个答案区
-7. 按钮没在答案区, 但是目标区域已经有按钮了
+7. 按钮没在答案区, 但是目标区域已经有按钮了
+
+
+## 学习计划拖拽逻辑
+1. 正确就播放正确的语音, 圆钮上出现正确标志
+2. 失败时播放失败的语音,圆钮晃动两下回到起始位置

+ 262 - 119
src/components/luojigou-board/luojigou-board.vue

@@ -1,9 +1,9 @@
 <template>
   <view class="luojigou-board" >
-    <view class="board" >
+    <view class="board" :style="{scale: adaptatio, transformOrigin: '50% 0%'}" >
       <image class="board-img" :src="boardUrl" />
       <view class="board-header" id="game-label">
-        <view class="trumpt" @click="playDescAudio">
+        <view class="trumpt" @click="emits('playAudio')">
           <image class="dog" :src="staticImg.trumptDog"   />
           <image
             class="trumpt-icon"
@@ -26,45 +26,89 @@
           <view class="ans" ref="ansRef" id="ansRef">
             <image :src="props.board.ansUrl" alt="" />
           </view>
-          <view class="mark-button" >
-            <image 
-              v-for="item in props.board.buttons"
-              class="movable-image" 
-              :style="{willChange: 'transform', top: item.y + 'px', left: item.x + 'px' }"  
-              :src="getButtonUrlByColor(item.color)" 
-            />
-          </view>
       </view>
+
+    </view>
+
+    <view
+      class="mark-button" 
+      :style="{ 
+        width: 357 * rate + 'rpx', 
+        height: 466 * rate + 'rpx', 
+        top: 102 * rate + 'rpx',
+        left: '50%', transform: 'translateX(-50%)'
+     }" 
+    >
+      <view
+        v-for="item in props.board.buttons"
+        class="movable-image" 
+        :style="{
+          willChange: 'transform', 
+          top: Number(item.y) * rate + 'rpx', 
+          left: Number(item.x) * rate + 'rpx',
+          width: 46 * rate + 'rpx', 
+          height: 46 * rate + 'rpx',
+          backgroundColor: item.color,
+          borderRadius: '50%'
+        }"  
+      />
     </view>
 
-    <movable-area v-if="props.cardType !== undefined" class="movable-area" id="movableAreaRef" >
+    <movable-area 
+      v-if="props.cardType !== undefined" 
+      class="movable-area" 
+      id="movableAreaRef" 
+      :style="{ 
+        width: 357 * rate + 'rpx', 
+        height: 466 * rate + 'rpx',
+        top: 102 * rate + 'rpx', 
+        left: '50%', 
+        transform: 'translateX(-50%)'
+      }"
+    >
       <movable-view
         v-for="item in buttons"
         :key="item.id"
+        :disabled="state.disabled"
         :x="item.x"
         :y="item.y"
         damping="100"
         direction="all"
         class="movable-view"
-        :style="{zIndex: item.zIndex}"
+        :style="{
+          zIndex: item.zIndex, 
+          width: 46 * rate + 'rpx', 
+          height: 46 * rate + 'rpx',
+        }"
         @touchend="touchend($event, item)"
         @touchstart="touchStart(item)"
       >
-        <image class="movable-image" :style="{willChange: 'transform'}"  :src="item.url" />
+        <image 
+          :id="`rock-id-${item.id}`"
+          :class="`movable-image `" 
+          :style="{willChange: 'transform', transformOrigin: `center bottom`}"  
+          :src="item.url" 
+        />
+        
+        <image
+          v-if="item.ans"
+          class="success-flag"
+          :src="staticImg.successFlag"
+        />
       </movable-view>
     </movable-area>
   </view>
 </template>
 
 
-<script setup lang="ts">
+<script setup lang="ts" name="luojigou-board" >
 import { defineProps, reactive, ref, computed, getCurrentInstance, defineEmits } from 'vue'
-
-import { useStaticImg, useSchedulerOnce, useQueryElInfo } from '@/hooks/index'
+import { useStaticImg, useSchedulerOnce, useQueryElInfo, useAdaptationIpadAndPhone } from '@/hooks/index'
 import type { CardModeEnum } from '@/enum/constant';
+import { useCalcStartStore, useCalcQuantityStore } from '@/store/index';
 
 interface Buttons {
-  id: string,
+  id: number,
   x: number,
   y: number,
   initX: number,
@@ -72,25 +116,43 @@ interface Buttons {
   url: string,
   color: API.Color,
   index: number,
-  zIndex: number
+  zIndex: number,
+  ans: API.Color | null
 }
 
 interface IProps {
-  cardType: 0 | 1,
+  cardType: 0 | 1, // 0 四钮题卡 | 1 六钮题卡
   board: API.Board,
   mode: CardModeEnum,
   cardDesc: string,
-  playLoading: boolean
+  playLoading: boolean,
+  getExpose: (records: any) => void
 }
 
 const staticImg = useStaticImg()
 
+const calcStartStore = useCalcStartStore()
+
+const adaptatio = useAdaptationIpadAndPhone()
+
+const calcQuantityStore = useCalcQuantityStore()
+
 const props = defineProps<IProps>()
 
-const emits = defineEmits(['playAudio'])
+const emits = defineEmits(['playAudio', 'submit'])
+
+const state = reactive({
+  zIndex: 0, // 暂存zIndex
+  disabled: false, // 是否禁用moveview
+  correctQuantity: 0, // 正确数量()
+  totalQuantity: 0 // 总数量 (移动按钮数量)
+})
+
 
 const boardUrl = props.cardType == 1 ? staticImg.boardSix : staticImg.boardFour
 
+calcStartStore.total = props.cardType == 1 ? 6 : 4
+
 const ansRef = ref<UniApp.NodeInfo>()
 const quesRef = ref<UniApp.NodeInfo>()
 const movableAreaRef = ref<UniApp.NodeInfo>()
@@ -99,28 +161,20 @@ useQueryElInfo('#quesRef', (nodeInfo) => quesRef.value = nodeInfo as UniApp.Node
 useQueryElInfo('#ansRef', (nodeInfo) => ansRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
 useQueryElInfo('#movableAreaRef', (nodeInfo) => movableAreaRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
 
-const state = reactive({
-  touchId: '',
-  ansHeight: 0,
-  quesWidth: 0,
-  pos: {x: 0, y: 0},
-  zIndex: 0
-})
-
+const baseRatio = adaptatio
 
+const rate = baseRatio * 2
 
-// 播放题卡描述的语音
-const playDescAudio = () => emits('playAudio')
+const { windowWidth, windowHeight } = uni.getSystemInfoSync()
 
-const getButtonUrlByColor = (color: API.Color) => {
-  return staticImg[color]
-}
+const unit = windowWidth / windowHeight > 0.6 ? 1 : 0.5
 
 const ansItemHeight = computed(() => Math.floor(ansRef.value?.height! / copies))
 
 const TPos = (x: number, y: number) => {
   
-  return {x: x - movableAreaRef.value?.left! - 23 , y: y - movableAreaRef.value?.top! - 23 }
+  return { x: x - movableAreaRef.value?.left! - 23 * rate * unit , y: y - movableAreaRef.value?.top! - 23 * rate * unit }
+  
 }
 
 const getButtonIndex = (_y: number) =>  Math.floor(_y / ansItemHeight.value)
@@ -129,7 +183,7 @@ const getButtonPosByIndex = (index: number) => {
 
   const x = ansRef.value?.width! + quesRef.value?.width! 
  
-  const y = ansItemHeight.value * index + ansItemHeight.value / 2 - 23 
+  const y = ansItemHeight.value * index + ansItemHeight.value / 2 - 23 * rate * unit
 
   return { x, y }
 }
@@ -137,28 +191,38 @@ const getButtonPosByIndex = (index: number) => {
 // 几钮模板
 const copies = props.cardType === 0 ? 4 : 6
 
-const VSpace = props.cardType === 0 ? 70 : 53
+const VSpace = props.cardType === 0 ? 23 * rate * unit + 46 * rate * unit : 6 * rate * unit + 46 * rate * unit
+
+const Y = 435 * rate
+
+const getX = (index: number) => index * VSpace + 13 * rate * unit
 
 // 注意一下四钮 用哪几个颜色的按钮
 let buttons = reactive<Buttons[]>([
-  {id: "1",  x: 0 * VSpace + 16, y: 430, initX: 0 * VSpace + 16, initY: 430,  zIndex: 0, index: -1, url: staticImg.red, color: 'red' },
-  {id: "2",  x: 1 * VSpace + 16, y: 430, initX: 1 * VSpace + 16, initY: 430,  zIndex: 0, index: -1, url: staticImg.blue, color: 'blue' },
-  {id: "3",  x: 2 * VSpace + 16, y: 430, initX: 2 * VSpace + 16, initY: 430,  zIndex: 0, index: -1, url: staticImg.green, color: 'green' },
-  {id: "4",  x: 3 * VSpace + 16, y: 430, initX: 3 * VSpace + 16, initY: 430,  zIndex: 0, index: -1, url: staticImg.orange, color: 'orange' },
-  {id: "5",  x: 4 * VSpace + 16, y: 430, initX: 4 * VSpace + 16, initY: 430,  zIndex: 0, index: -1, url: staticImg.yellow, color: "yellow" },
-  {id: "6",  x: 5 * VSpace + 16, y: 430, initX: 5 * VSpace + 16, initY: 430,  zIndex: 0, index: -1, url: staticImg.purple, color: 'purple' },
+  {id: 0, ans: null, x: getX(0), y: Y * rate, initX: getX(0), initY: Y * rate,  zIndex: 0, index: -1, url: staticImg.red, color: 'red' },
+  {id: 1, ans: null, x: getX(1), y: Y * rate, initX: getX(1), initY: Y * rate,  zIndex: 0, index: -1, url: staticImg.blue, color: 'blue' },
+  {id: 2, ans: null, x: getX(2), y: Y * rate, initX: getX(2), initY: Y * rate,  zIndex: 0, index: -1, url: staticImg.green, color: 'green' },
+  {id: 3, ans: null, x: getX(3), y: Y * rate, initX: getX(3), initY: Y * rate,  zIndex: 0, index: -1, url: staticImg.orange, color: 'orange' },
+  {id: 4, ans: null, x: getX(4), y: Y * rate, initX: getX(4), initY: Y * rate,  zIndex: 0, index: -1, url: staticImg.yellow, color: "yellow" },
+  {id: 5, ans: null, x: getX(5), y: Y * rate, initX: getX(5), initY: Y * rate,  zIndex: 0, index: -1, url: staticImg.purple, color: 'purple' },
 ])
 
 buttons.splice(props.cardType == 1 ?  buttons.length : buttons.length - 2, buttons.length)
 
 // 让按钮回到原位
-const initButtonPos = (index: number) => {
+const disPatchButtonGoInitPos = (index: number) => {
     
   useSchedulerOnce(() => {
-    buttons[index].x = buttons[index].initX
-    buttons[index].y = buttons[index].initY
+    buttons[index].x = buttons[index].initX + Math.random()
+    buttons[index].y = buttons[index].initY + Math.random()
     buttons[index].index = -1
+    buttons[index].ans = null
   })
+
+  useSchedulerOnce(() => {
+    state.disabled = false
+  }, 200)
+
 }
 
 const touchStart = (item: Buttons) => {
@@ -168,40 +232,40 @@ const touchStart = (item: Buttons) => {
   })
 }
 
+// 按钮脱手
 const touchend = (ev: TouchEvent, item: Buttons) => {
-    
+
+  if (state.disabled) return
+
   const { x: itemX, y: itemY } = TPos(ev.changedTouches[0].pageX, ev.changedTouches[0].pageY)
 
+  console.log("itemY:", itemY);
+  
+
   // 返回原点 (没有放在答案区, 按钮回到初始位置)
   if (itemX < quesRef.value?.width! || itemY > ansRef.value?.height! ) {
-
-    useSchedulerOnce(() => {
-      item.x = item.initX + Math.random()
-      item.y = item.initY + Math.random()
-    })
-    
+    disPatchButtonGoInitPos(item.id)
   } else  {
   
     const index = getButtonIndex(itemY)
+
+    console.log('index:', index);
+    
     
     const { x: targetX, y: targetY } = getButtonPosByIndex(index)
     
     //  直接点击答案区的按钮,答案区按钮回到初始位置
     if (item.x == targetX && item.y == targetY) {
-      useSchedulerOnce(() => {
-        item.x = item.initX + Math.random()
-        item.y = item.initY + Math.random()
-        item.index = -1
-      })
+      disPatchButtonGoInitPos(item.id)
     } else {
 
       // 两个按钮都在答案区, 直接进行按钮之间的交换
       const isHasIndex = buttons.findIndex(button => button.index == index && button.id !== item.id ) 
-
+      
       if (isHasIndex >= 0) { // 答案区的目标区域存在按钮
      
         if (item.index == -1) { // 答案区的目标区域存在按钮 操作按钮是从待操作区域移过来的 目标按钮回到原位
-          initButtonPos(isHasIndex)
+          disPatchButtonGoInitPos(isHasIndex)
         } else {  // 答案区的目标区域存在按钮 操作按钮是和目标按钮互换位置
           useSchedulerOnce(() => {
             const oldPos = getButtonPosByIndex(item.index)
@@ -212,32 +276,74 @@ const touchend = (ev: TouchEvent, item: Buttons) => {
         }
       }
 
+      // 将按钮放置到答案区对应的位置
       useSchedulerOnce(() => {
         item.x = targetX
         item.y = targetY
         item.index = index
       })
+
+      checkAns(index, item)
+
     }
   }
 }
 
+// 判断答案 (学习计划适用逻辑)
+const checkAns = (index: number, itemData: Buttons) => {
+  const targetData = props.board.ansList
+  console.log(targetData, index, itemData);
+  
+  // 移动到了正确位置
+  if (targetData[index].color === itemData.color) {
+    itemData.ans = targetData[index].color
+    calcQuantityStore.correctQuantity++
+  } else { // 移动到了錯誤位置
+
+    calcStartStore.wrongCount++
+
+    itemData.ans = null
+
+    const rockNode = document.getElementById(`rock-id-${itemData.id}`)
+
+    rockNode?.classList.add('rock-button-ani')
+
+    state.disabled = true
+
+    useSchedulerOnce(() => {
+      disPatchButtonGoInitPos(itemData.id)
+      
+      rockNode?.classList.remove('rock-button-ani')
+    }, 1000)
+  }
+  
+  calcQuantityStore.totalQuantity++
+
+  // 全部正确后触发提交答案的emit
+
+  const ansLen = buttons.filter( button => button.ans).length
+
+  if ( ansLen === ( props.cardType === 0 ? 4 : 6 )) {
+    emits('submit')
+  }
+}
 
 </script>
 
 <style lang="less" scoped >
 .luojigou-board {
-  width: 714px;
-  height: 617px;
+  width: 714rpx;
+  height: 1234rpx;
   position: relative;
   display: flex;
   justify-content: center;
   .board {
-      width: 357px;
-      height: 617px;
+      width: 714rpx;
+      height: 1234rpx;
       position: relative;
       .board-img {
-        width: 357px;
-        height: 617px;
+        width: 100%;
+        height: 100%;
         position: absolute;
         top: 0;
         left: 0;
@@ -245,62 +351,59 @@ const touchend = (ev: TouchEvent, item: Buttons) => {
       }
       .board-header {
         width: 100%;
-        height: 148px;
+        height: 148rpx;
         position: relative;
-        top: 7px;
+        top: 7rpx;
         z-index: 12;
-        // margin-bottom: 14rpx;
         .trumpt {
-          width: 84px;
-          height: 84px;
+          width: 96rpx;
+          height: 96rpx;
           position: relative;
-          top: 44px;
-          left: 54px;
+          top: 44rpx;
+          left: 54rpx;
           .dog {
-            width: 84px;
-            height: 84px;
+            width: 96rpx;
+            height: 96rpx;
             position: absolute;
             z-index: 1;
           }
           .trumpt-icon {
-            width: 40px;
-            height: 40px;
+            width: 40rpx;
+            height: 40rpx;
             position: absolute;
             z-index: 2;
-            right: -12px;
-            bottom: -6px;
+            right: -12rpx;
+            bottom: -6rpx;
           }
         }
         .tip {
-          width: 506px;
-          height: 72px;
-          font-size: 24px;
+          width: 490rpx;
+          height: 72rpx;
+          font-size: 24rpx;
           font-family: PingFangSC-Medium, PingFang SC;
           font-weight: 500;
           color: #ffffff;
           position: absolute;
-          top: 50px;
-          left: 156px;
-          // overflow: hidden;
-          // overflow-y: auto;
+          top: 50rpx;
+          left: 176rpx;
         }
       }
       .card {
-        width: 303px;
-        height: 386px;
-        border-radius: 20px;
+        width: 606rpx;
+        height: 772rpx;
+        border-radius: 40rpx;
         overflow: hidden;
         display: flex;
         justify-content: space-between;
         position: absolute;
-        top: 103px;
-        left: 13px;
+        top: 206rpx;
+        left: 26rpx;
         z-index: 2;
       }
       .ques {
-        width: 225px;
-        height: 386px;
-        border-right: 1px solid #006CAA;
+        width: 450rpx;
+        height: 772rpx;
+        border-right: 2rpx solid #006CAA;
         image {
           width: 100%;
           height: 100%;
@@ -308,12 +411,12 @@ const touchend = (ev: TouchEvent, item: Buttons) => {
         }
       }
       .ans {
-        width: 85px;
+        width: 170rpx;
         height: 100%;
         display: flex;
         flex-direction: column;
-        border-top-right-radius: 20px;
-        border-bottom-right-radius: 20px;
+        border-top-right-radius: 40rpx;
+        border-bottom-right-radius: 40rpx;
         overflow: hidden;
         background-color: #fff;
         image {
@@ -323,29 +426,15 @@ const touchend = (ev: TouchEvent, item: Buttons) => {
           }
       }
 
-      .mark-button {
-        width: 100%;
-        height: 100%;
-        position: absolute;
-        top: 0px;
-        left: 0px;
-        image {
-          position: absolute;
-          width: 46px;
-          height: 46px;
-          display: block;
-        }
-      }
-
       .ans .ans-item:last-child {
         border-bottom: none
       }
       .buttons {
         position: relative;
-        top: 100px;
-        left: 10px;
-        width: 340px;
-        height: 500px;
+        top: 200rpx;
+        left: 20rpx;
+        width: 680rpx;
+        height: 1000rpx;
         image {
           position: absolute;
           left: 0;
@@ -354,12 +443,30 @@ const touchend = (ev: TouchEvent, item: Buttons) => {
       }
   }
 
+
+  .mark-button {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    .movable-image {
+      position: absolute;
+      width: 92rpx;
+      height: 92rpx;
+      display: block;
+    }
+  }
+  
+
   .movable-area {
     width: 714rpx;
-    height: 985rpx;
+    height: 984rpx;
     background-color: transparent;
     position: absolute;
-    top: 96px;
+    top: 192rpx;
+    left: 50%;
+    transform: translateX(-50%);
     touch-action: none;
     left: 0px;
     z-index: 30;
@@ -376,17 +483,53 @@ const touchend = (ev: TouchEvent, item: Buttons) => {
         width: 100%;
         height: 100%;
         transition: width 0.2s;
+        z-index: 1;
+      }
+      
+      .success-flag {
+        width: 36rpx;
+        height: 26rpx;
+        display: block;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        z-index: 2;
       }
     }
   }
 
   .opra {
     width: 100vw;
-    height: 708px;
+    height: 1216rpx;
     background: url('../../assets/boardBg.png');
     display: flex;
     justify-content: center;
-    padding-top: 23px;
+    padding-top: 46rpx;
+  }
+}
+
+
+.rock-button-ani {
+  animation: rock 1s;
+}
+
+@keyframes rock {
+  0% {
+    rotate: 0deg;
+  }
+  25% {
+   
+    rotate: -10deg;
+  }
+  50% {
+    rotate: 10deg;
+  }
+  75% {
+    rotate: -10deg;
+  }
+  100% {
+    rotate: 0deg;
   }
 }
 

+ 29 - 25
src/components/navbar/navbar.vue

@@ -5,14 +5,14 @@
         <view class="level-count">
           <view
             class="level-dot"
-            v-for="item in props.levelCount"
+            v-for="item in state.levelCount"
             :key="item"
-            :style="{backgroundColor: item <= props.progress ? '#1191E7' : '#FFCC6A' }"
+            :style="{backgroundColor: item <= state.progress ? '#1191E7' : '#FFCC6A' }"
           />
         </view>
         <view class="countdown" >
           <image :src="staticImg.clock" />
-          {{props.countdownTotal}}
+          {{state.countdownTotal}}
         </view>
       </view>
     </view>
@@ -20,12 +20,14 @@
 </template>
 
 <script lang="ts" setup name="navbar" >
-import { reactive } from "vue"
+import { reactive, defineProps } from "vue"
 import { useNavbarInfo, useScheduler, useStaticImg } from '@/hooks/index';
 import { CardModeEnum } from "@/enum/constant";
 
 const navbarInfo = useNavbarInfo()
+
 const staticImg = useStaticImg()
+
 interface IPorps {
   mode: CardModeEnum
   levelCount: number  // 总题卡数
@@ -40,17 +42,19 @@ const initProps: IPorps = reactive({
   mode: CardModeEnum.PREVIEW
 })
 
-const props = initProps
+const props = defineProps<IPorps>()
+
+const state = reactive<IPorps>(props)
 
 const stx = useScheduler(() => {
-  props.countdownTotal--
-  if (props.countdownTotal <= 0) {
+  state.countdownTotal--
+  if (state.countdownTotal <= 0) {
     stx.stop()
   }
 }, 1000)
 
 
-if (props.mode === CardModeEnum.OPRA) {
+if (state.mode === CardModeEnum.OPRA) {
   stx.start()
 }
 
@@ -72,47 +76,47 @@ const contentStyles = {
     width: 100%;
     height: 100%;
     background: #F9BF4F;
-    border-radius: 0px 0px 20px 20px;
+    border-radius: 0px 0px 40rpx 40rpx;
     .content {
       display: flex;
       align-items: center;
       position: absolute;
       .level-count {
         background: #FAA400;
-        box-shadow: inset 0px 1px 3px 0px rgba(162,76,0,0.25);
-        border-radius: 100px;
-        padding: 8px 10px;
+        box-shadow: inset 0px 2rpx 6rpx 0px rgba(162,76,0,0.25);
+        border-radius: 200rpx;
+        padding: 16rpx 20rpx;
         display: inline-block;
         box-sizing: border-box;
-        margin-right: 26px;
+        margin-right: 52rpx;
       .level-dot {
-        width: 16px;
-        height: 16px;
+        width: 32rpx;
+        height: 32rpx;
         border-radius: 50%;
-        margin-right: 8px;
+        margin-right: 16rpx;
         display: inline-block;
       }
       }
       .countdown {
-        font-size: 14px;
+        font-size: 28rpx;
         font-family: PingFangSC-Medium, PingFang SC;
         font-weight: 500;
         color: #FFFFFF;
-        width: 65px;
-        height: 32px;
+        width: 130rpx;
+        height: 64rpx;
         background: #FAA400;
-        box-shadow: inset 0px 1px 3px 0px rgba(162,76,0,0.25);
-        border-radius: 0px 100px 100px 0px;
+        box-shadow: inset 0px 2rpx 6rpx 0px rgba(162,76,0,0.25);
+        border-radius: 0px 200rpx 200rpx 0px;
         display: flex;
         justify-content: center;
         align-items: center;
         position: relative;
         image {
-          width: 66px;
-          height: 80px;
+          width: 66rpx;
+          height: 80rpx;
           position: absolute;
-          left: -32px;
-          top: -6px;
+          left: -32rpx;
+          top: -6rpx;
           display: block;
         }
       }

+ 185 - 32
src/components/rive-ani/rive-ani.vue

@@ -1,29 +1,91 @@
 <template>
-  <view class="rive-ani"  id="rive-ani" @click="addWisdomCoin" >
-    <view class="coin-total"  >
-      <img :src="staticImg.coinTotal" alt="">
+  <view class="rive-ani" id="rive-ani" v-if="state.comVisable" >
+    <view class="coin-total" >
+      <view class="coin-logo" id="coin-logo" :animation="state.heartbeatAni" >
+        <img :src="staticImg.coinTotal" alt="">
+      </view>
       <span>{{userStore.wisdomCoin}}</span>
     </view>
+    <view class="coin-ani" id="coin-ani"  v-show="state.visible"  >
+      <view
+        class="coin-item"
+        v-for="item in state.cointCount"
+        :key="item"
+        :animation="state.animation[item - 1]"
+        :id="`coin-item-${item}`"
+      >
+        <img  :src="staticImg.coinAni" alt=""   >
+      </view>
+      <view class="coin-count" id="coin-count" >+ {{state.cointCount}}</view>
+    </view>
   </view>
 </template>
 
 
 <script setup lang="ts" name="rive " >
-import { onMounted, defineProps } from 'vue'
+import { onMounted, ref, reactive, getCurrentInstance, defineExpose, nextTick} from 'vue'
 import { Rive } from "@rive-app/canvas"
-import { useStaticImg } from '@/hooks/index'
+import { useStaticImg, useQueryElInfo, useSchedulerOnce } from '@/hooks/index'
 import { useUserStore } from '@/store'
 
+enum AniEnum {
+  'one',
+  'two',
+  'three',
+  'four',
+  'five',
+  'six'
+}
+
+/**
+ * 动画逻辑
+ * 1. 得几颗星就控制rive动画给几颗星
+ * 2. 然后rive动画渐隐
+ * 3. 出现金币 +n 的字样
+ * 4. 金币飞到右上角金币框
+ */
+
 const staticImg = useStaticImg()
 
 const userStore = useUserStore()
 
-const initProps = {
-  startCount: 2
+interface State {
+  rive: Rive,
+  animation: any, 
+  visible: boolean,
+  heartbeatAni: any,
+  cointCount: number,
+  comVisable: boolean
 }
 
-const props = initProps
+const state = reactive<Partial<State>>({
+  animation: [],
+  visible: false,
+  heartbeatAni: null,
+  cointCount: 0,
+  comVisable: false
+})
+
+const emit = defineEmits(['onEnd'])
+
+const coinLoginRef = ref()
 
+let rive: Rive
+
+let _resolve: any
+
+const initHeartbeatAni = () => {
+
+  var animation = uni.createAnimation({
+    timingFunction: "linear",
+    transformOrigin: "0% 0%"
+  });
+
+  return animation.scale(1.1, 1.1).step({duration: 100}).scale(1, 1).step({duration: 100}).export()
+
+}
+
+// 1. 初始化rive动画
 const initRive = () => {
 
   const canvas = document.createElement('canvas')
@@ -35,41 +97,99 @@ const initRive = () => {
 
   canvas.style.position = 'absolute'
   canvas.style.top = '50%'
-  canvas.style.left = '50%'
+  canvas.style.left = '45%'
   canvas.style.transform = 'translate(-50%, -50%)'
   
   document.getElementById('rive-ani')?.appendChild(canvas)
-
-  console.log(document.getElementById('rive-ani'));
   
+  rive = new Rive({
+    src: "https://res-game.luojigou.vip/coin1.riv",
+    canvas: canvas,
+    autoplay: false,
+    stateMachines: "bumpy",
+    artboard: 'six-xing',
+    onLoad: () => {
+      rive.resizeToCanvas()
+      rive.play(AniEnum[state.cointCount!])
+    },
+    onStop: () => {
+      fadeOutRive()
+      useSchedulerOnce(() => {
+        state.visible = true
+        goBezier()
+      }, 1000)
+      useSchedulerOnce(() => document.getElementById('coin-count')!.style.display = 'none', 500)
+    }
+  });
+
+}
+
+const createAnimationStep = (animation: UniApp.Animation, count: number) => {
+
+  for (let i = 0; i < count; i++) {
+    state.animation.push(animation.step({duration: 1000 + i * 600}).export())
+    useSchedulerOnce(() => {
+      document.getElementById(`coin-item-${i + 1}`)!.style.display = 'none'
+      state.heartbeatAni = initHeartbeatAni()
+    }, 1000 + i * 600)
+  }
+
+}
+
+// 2. rive动画渐隐消失
+const fadeOutRive = () => {
+  const riveCanvas = document.getElementById('rive-canvas')!
+  riveCanvas.style.opacity = '0'
+  riveCanvas.style.transition = '1s'
+}
+
+const goBezier = () => {
   
-  const river = new Rive({
-        src: "https://res-game.luojigou.vip/coin1.riv",
-        canvas: canvas,
-        autoplay: true,
-        stateMachines: "bumpy",
-        artboard: 'six-xing',
-        onLoad: () => {
-          console.log(river);
-          // console.log(river.animationNames);
-          river.resizeToCanvas()
-          river.play("four")
-          // river.resizeToCanvas()
-        }
+  var animation = uni.createAnimation({
+    timingFunction: "linear",
+    transformOrigin: "0% 0%"
   });
+
+  animation
+  .top(coinLoginRef.value.top)
+  .left(coinLoginRef.value.left)
+  .scale(0.25, 0.25)
+
+  createAnimationStep(animation, state.cointCount!)
+
+  useSchedulerOnce(() => {
+    console.log('触发emit');
+    _resolve(2)
+    state.comVisable = false
+  }, 1200 + state.cointCount! * 600)
+
 }
 
-const addWisdomCoin = () => {
-  console.log(userStore.wisdomCoin++);
-  // userStore.$state.wisdomCoin++
-  // console.log(userStore.$state.wisdomCoin);
+const start = (starCount: number) => {
+  state.comVisable = true
+  state.cointCount = starCount
+  useSchedulerOnce(initRive)
+  
+  nextTick(() => useQueryElInfo('#coin-logo', (res) => coinLoginRef.value = res, getCurrentInstance()!, false))
+
+  return new Promise((resolve) => {
+    _resolve = resolve
+  })
+
 }
 
+defineExpose({
+  start: start
+})
 
 onMounted(() => {
-  initRive()
+
+  
+
 })
 
+
+
 </script>
 
 
@@ -96,10 +216,14 @@ onMounted(() => {
     align-items: center;
     padding: 0 8px;
     box-sizing: border-box;
-    img {
-      width: 20px;
-      height: 20px;
+    .coin-logo {
+      img {
+        width: 20px;
+        height: 20px;
+        display: block;
+      }
     }
+    
     span {
       font-size: 18px;
       font-family: PingFangSC-Semibold, PingFang SC;
@@ -109,9 +233,38 @@ onMounted(() => {
       display: block;
     }
   }
+  .coin-ani {
+    width: 100vw;
+    height: 100vh;
+    position: absolute;
+    top: 0%;
+    left: 0%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .coin-item {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+    img {
+      width: 152rpx;
+      height: 152rpx;
+    }
+    .coin-count {
+      font-size: 56rpx;
+      font-family: PingFangSC-Semibold, PingFang SC;
+      font-weight: 600;
+      color: #FF8024;
+      margin-left: 250rpx;
+      margin-bottom: 20rpx;
+    }
+  }
 }
 
 .uni-canvas-canvas {
   width: 1000px !important;
 }
+
 </style>

+ 38 - 14
src/hooks/index.ts

@@ -70,8 +70,6 @@ export const useSchedulerOnce = (
 
 }
 
-
-
 /**
  * 它返回一个对象,其中包含导航栏的高度、导航栏内内容的高度以及导航栏的顶部位置。
  * @returns 具有 height、contentHeight 和 top 属性的对象。
@@ -83,10 +81,10 @@ export const useNavbarInfo = () => {
   if (uniPlatform == "web") {
 
     return {
-      height: "70px",
-      contentHeight: '32px',
-      top: '19px',
-      left: '20px'
+      height: "140rpx",
+      contentHeight: '64rpx',
+      top: '38rpx',
+      left: '40rpx'
     }
   
   } else {
@@ -95,7 +93,6 @@ export const useNavbarInfo = () => {
     console.log("(safeArea!.top + bounding.height):", (safeArea!.top + bounding.height));
     
     return {
-      // height: (safeArea!.top + bounding.height) + 'px',
       height: 104 + 'px',
       contentHeight: bounding.height + (bounding.top - systemInfo.statusBarHeight!) * 2 + "px",
       top: systemInfo.statusBarHeight!  + "px",
@@ -111,15 +108,20 @@ export const useNavbarInfo = () => {
  * @param cb - 回调函数
  * @param {ComponentInternalInstance} vm - 组件实例
  */
-export const useQueryElInfo = (id: string, cb: (params: UniApp.NodeInfo | UniApp.NodeInfo[] ) => void, vm?: ComponentInternalInstance) => {
-
-  onMounted(() => {
+export const useQueryElInfo = (
+  id: string,
+  cb: (params: UniApp.NodeInfo | UniApp.NodeInfo[] ) => void,
+  vm?: ComponentInternalInstance,
+  immediate: boolean = true
+) => {
+
+  const queryNode = () => {
     const query = vm ? uni.createSelectorQuery().in(vm) :  uni.createSelectorQuery();
     query.select(id).boundingClientRect(data => cb(data)).exec();
-  })
-  
-}
+  }
 
+  immediate ? onMounted(() => queryNode()) : queryNode()
+}
 
 /**
  * 它创建一个音频实例,并返回一个具有 destroy、play 和 onPlayend 方法的对象。
@@ -127,7 +129,7 @@ export const useQueryElInfo = (id: string, cb: (params: UniApp.NodeInfo | UniApp
  * @returns 具有三个属性的对象:destroy、play 和 onPlayend。
  */
 export const useAudioMange = (src?: string) => {
- 
+
   const audioInstance = ref< UniApp.InnerAudioContext>()
  
   audioInstance.value = uni.createInnerAudioContext()
@@ -170,3 +172,25 @@ export const usePlatform = (): DEVICE.Platform => {
   return uniPlatform as DEVICE.Platform
   
 }
+
+
+/**
+ * 获取多设备动态适配的比率
+ * @example
+ *  const rate = useAdaptationIpadAndPhone()
+ *  node.width = node.width * rate * 2 + 'rpx
+ */
+export const useAdaptationIpadAndPhone = () => {
+  
+  const { windowWidth, windowHeight } = uni.getSystemInfoSync()
+
+  const rate = windowWidth / windowHeight
+  
+  if ( rate > 0.6 ) {
+    return 667 / ( windowHeight )
+  } else {
+    return 1 
+  }
+
+}
+

+ 3 - 0
src/main.ts

@@ -2,6 +2,9 @@ import { createSSRApp } from "vue";
 import App from "./App.vue";
 import * as Pinia from 'pinia'
 import staticImg from '@/utils/static'
+import VConsole from "vconsole"
+
+
 
 export function createApp() {
   const app = createSSRApp(App);

+ 117 - 17
src/pages/GameView/index.vue

@@ -2,11 +2,18 @@
   <view
     class="game-view"
     id="game-view"
-    :style="`pointer-events: ${state.mode === CardModeEnum.PREVIEW ? 'none' : 'all'}`" 
+    :style="`pointer-events: ${state.mode === CardModeEnum.PREVIEW ? 'none' : 'all'}`"
   >
-    <navbar />
-    <view class="luojigou-board-box"  >
+    <navbar 
+      :="{
+        ...navbarState,
+        levelCount: state.cardIds.length
+      }"
+    />
+    <view class="luojigou-board-box">
+
       <luojigou-board
+        :key="state.cardId"
         v-if="state.card?.board?.quesUrl"
         :cardType="state.card?.cardType"
         :board="state.card?.board"
@@ -14,11 +21,13 @@
         :cardDesc="state.card.cardDesc"
         :playLoading="state.playLoading"
         @playAudio="playAudio"
+        @submit="onSubmitCard"
       />
+
     </view>
-    <div></div>
-    <!-- <rive-ani /> -->
-   
+
+    <rive-ani ref="riveAniRef" />
+
   </view>
 </template>
 
@@ -27,29 +36,41 @@
 import { CardModeEnum, OpraModeEnum } from '@/enum/constant';
 import { onLoad } from '@dcloudio/uni-app';
 import { reactive, onMounted } from 'vue'
-import { getCardDetailById } from '@/api/card'
+import { getCardDetailById, submitlearnPortAns } from '@/api/card'
 import { getCollectionDetailById } from '@/api/collection'
-import { useAudioMange } from '@/hooks/index'
-import { usePracticeStore } from '@/store'
+import { useAudioMange, useScheduler } from '@/hooks/index'
+import { usePracticeStore, useOpraRecordStore, useGameCountdownStore, useCalcQuantityStore } from '@/store'
 import riveAni from '@/components/rive-ani/rive-ani.vue';
+import { ref } from 'vue';
+
 
+/**
+ *  README.md 里有详细的参数说明
+ */
 export interface QueryParams {
   mode: CardModeEnum,
   collectionId: string,
   cardId?: string,
-  opraMode: OpraModeEnum
+  opraMode: OpraModeEnum,
 }
 
 interface State extends QueryParams {
   card: Partial<API.Card> | null,
   cardIds: string[],
-  playLoading: boolean
+  playLoading: boolean,
+  userCardAnswerList: any[]
 }
 
 const atx = useAudioMange()
 
 const practiceStore = usePracticeStore()
 
+const opraRecordStore = useOpraRecordStore()
+
+const gameCountdownStore = useGameCountdownStore()
+
+const calcQuantityStore = useCalcQuantityStore()
+
 onLoad(query => {
   const options = query as QueryParams
   state.mode = options.mode
@@ -57,6 +78,14 @@ onLoad(query => {
   if (options.cardId) state.cardId = options.cardId
 })
 
+const riveAniRef = ref()
+
+const navbarState = reactive({
+  progress: 1,
+  countdownTotal: 60,
+  mode: CardModeEnum.PREVIEW
+})
+
 const state = reactive<State>({
   opraMode: OpraModeEnum.COMPE,
   mode: CardModeEnum.PREVIEW,
@@ -71,17 +100,82 @@ const state = reactive<State>({
       ansList: []
     }
   },
-  playLoading: false
+  playLoading: false,
+  userCardAnswerList: []
 })
 
+const stx = useScheduler(() => {
+  navbarState.countdownTotal--
+  if (navbarState.countdownTotal <= 0) {
+    stx.stop()
+    onSubmitCard()
+  }
+}, 1000)
+
+// 控制游戏开始
+const gameStart = () => {
+  navbarState.countdownTotal = 60
+  stx.start()
+  playAudio()
+}
+
+// 提交单张题卡数据
+const onSubmitCard = async () => {
+
+  // 触发做题完成的动画
+  await riveAniRef.value.start(3)
+  
+  // 保存做题记录
+  opraRecordStore.push(calcQuantityStore)
+
+  // 保存做题时间
+  gameCountdownStore.sum(navbarState.countdownTotal)
+
+  // 触发游戏结束
+  if (opraRecordStore.length == 5) {
+    // 重置一些东西
+  }
+
+  // 切换题卡
+  navbarState.progress++
+
+  state.cardId = state.cardIds[navbarState.progress - 1]
+  console.log("state.cardId:", state.cardId);
+  console.log('触发接口');
+
+  GetCardDetailById()
+
+}
+
+// 提交题卡
+const gameCompleted = async () => {
+  // state
+  const $par = {
+    "attach": {
+      "itemId": "",
+      "recordId": ""
+    },
+    "data": {
+      "correctQuantity": 0,
+      "gameDuration": 0,
+      "totalQuantity": 0,
+      "userCardAnswerList": opraRecordStore.get(),
+      "winWisdomCoin": gameCountdownStore.get()
+    }
+  }
+
+  const data = await submitlearnPortAns($par)
+  
+}
+
 const playAudio = () => {
+  console.log('state.card?.audio:', state.card?.audio);
+  
   state.playLoading = true
   atx.play(state.card?.audio!)
   atx.onplayend(() => state.playLoading = false)
 }
 
-// Math.floor(Math.random() * cardId.length)
-
 // 获取试题集详情
 const GetCollectionDetailById = async () => {
   const { data } = await getCollectionDetailById(state.collectionId)
@@ -92,7 +186,7 @@ const GetCollectionDetailById = async () => {
   }
 
   if ( state.cardIds.length > 0) {
-    state.cardId = state.cardIds[0]
+    state.cardId = state.cardIds[navbarState.progress - 1]
     GetCardDetailById()
   }
 }
@@ -101,15 +195,21 @@ const GetCollectionDetailById = async () => {
 const GetCardDetailById = async () => {
   const { data } = await getCardDetailById(state.cardId!, state.mode)
   state.card = data
+  if (state.mode === CardModeEnum.OPRA) {
+    gameStart()
+  }
 }
 
 
+onMounted(async () => {
+
+  console.log('riveAniRef.value:', riveAniRef.value);
 
-onMounted(() => {
   if (state.mode === 'preview') {
     GetCardDetailById()
   } else {
     GetCollectionDetailById()
+    
   }
 })
 
@@ -128,7 +228,7 @@ onMounted(() => {
   .luojigou-board-box {
     width: 100vw;
     height: 708px;
-    background: url('http://nginx.test.luojigou.vip:8899/0/file/1637996006995918849.png');
+    background: url('https://app-resources-luojigou.luojigou.vip/Fqj0sowjX078sk3PgbBlSvT_Ti9R');
     display: flex;
     justify-content: center;
     padding-top: 23px;

+ 1 - 1
src/service/index.ts

@@ -9,7 +9,7 @@ interface Params {
   baseURL?: string
 }
 
-const BASEURL = 'http://local.luojigou.vip:8888'
+const BASEURL = import.meta.env.MODE == 'development' ? 'http://local.luojigou.vip:8888' : "https://open.api.luojigou.vip"
 
 export const request = async <T>(
   params: Params

+ 67 - 0
src/store/gameTool.ts

@@ -0,0 +1,67 @@
+import { ConstantLocalStorage } from '@/utils/constant'
+import { ConstantStore } from '@/utils/constant'
+import { defineStore } from 'pinia'
+import { reactive } from 'vue'
+import { useSessionStorageState } from 'vue-hooks-plus'
+
+
+/**
+ * 计算时间的store
+ */
+export const useGameCountdownStore = defineStore(ConstantStore.COUNTDOWN, () => {
+  
+  const [ countdown, setCountdown ] = useSessionStorageState(ConstantLocalStorage.COUNTDOWN, {
+    defaultValue: 0
+  })
+
+  const _sum = (time: number) => setCountdown(countdown.value! + time)
+
+  const _clear = () => setCountdown(0)
+
+  return {
+    sum: _sum,
+    get: () => countdown.value,
+    clear: _clear
+  }
+
+})
+
+/**
+ * 计算用户本张题卡获得的星星数
+ * @param wrongCount 错误次数
+ * @param totalStart 
+ */
+export const useCalcStartStore = defineStore(ConstantStore.STAR, () => {
+
+  const state = reactive({
+    wrongCount: 0,
+    total: 0
+  })
+
+  const _get = () => {
+    const r = state.total - state.wrongCount
+    return r <= 0 ? 1 : r
+  }
+
+  return {
+    ...state,
+    get: _get
+  }
+
+})
+
+
+export const useCalcQuantityStore = defineStore(ConstantStore.QUANTITY, () => {
+
+  const state = reactive({
+    correctQuantity: 0,
+    totalQuantity: 0
+  })
+
+  return {
+    ...state
+  }
+
+})
+
+

+ 5 - 1
src/store/index.ts

@@ -1,3 +1,7 @@
 export { usePracticeStore } from './practice'
 
-export { useUserStore } from './user' 
+export { useUserStore } from './user' 
+
+export { useOpraRecordStore } from './opraRecords'
+
+export { useGameCountdownStore, useCalcStartStore, useCalcQuantityStore } from './gameTool'

+ 26 - 0
src/store/opraRecords.ts

@@ -0,0 +1,26 @@
+import { ConstantLocalStorage } from '@/utils/constant'
+import { ConstantStore } from '@/utils/constant'
+import { defineStore } from 'pinia'
+import { useLocalStorageState } from 'vue-hooks-plus'
+
+
+export const useOpraRecordStore = defineStore(ConstantStore.OPRARECORDS, () => {
+
+  const [ state, setState ] = useLocalStorageState<API.LearnPlanRecords[]>(ConstantLocalStorage.OPRARECORDS, {
+    defaultValue: []
+  })
+
+  const _push = (records: any) => setState([...state.value!, records])
+
+  const _clear = () => setState([])
+
+  const _get = () => state.value
+
+  return {
+    push: _push,
+    get: _get,
+    clear: _clear,
+    length: state.value?.length
+  }
+
+})

+ 6 - 0
src/typing.d.ts

@@ -75,6 +75,12 @@ declare namespace API {
     onPlaying: () => void
   }
 
+  interface LearnPlanRecords {
+    capability: string,
+    correctQuantity: number,
+    totalQuantity: number
+  }
+
 }
 
 

+ 9 - 3
src/utils/constant.ts

@@ -1,13 +1,19 @@
 
 export enum ConstantLocalStorage {
-  "USER" = 'USER'
+  "USER" = 'USER',
+  "OPRARECORDS" = "OPRARECORDS",
+  'COUNTDOWN' = 'COUNTDOWN',
+  'STAR' = 'STAR'
 }
 
 
-
 export enum ConstantStore {
   'count' = 'count',
   'USER' = 'USER',
   'DEVICE' = 'DEVICE',
-  'NAVBARINFO' = 'NAVBARINFO'
+  'NAVBARINFO' = 'NAVBARINFO',
+  'OPRARECORDS' = 'OPRARECORDS',
+  'COUNTDOWN' = 'COUNTDOWN',
+  'STAR' = 'STAR',
+  'QUANTITY' = 'QUANTITY'
 }

+ 8 - 2
src/utils/static.ts

@@ -15,6 +15,8 @@ import step3 from '@/assets/5-6.png'
 import step4 from '@/assets/7-10.png'
 import step5 from '@/assets/9-12.png'
 import coinTotal from '@/assets/coin-total.png'
+import coinAni from '@/assets/coin-ani.png'
+import successFlag from '@/assets/success-flag.png'
 
 
 
@@ -39,7 +41,9 @@ export interface StaticImg {
   step4: string,
   step5: string,
   patternBg: string,
-  coinTotal: string
+  coinTotal: string,
+  coinAni: string,
+  successFlag: string
 }
 
 let staticImg: Partial<StaticImg> = {
@@ -63,7 +67,9 @@ let staticImg: Partial<StaticImg> = {
   trumptGif: 'https://app-resources-luojigou.luojigou.vip/FvTWQ036DVezuqiDuua4y3F9yzTJ',
   trumptPng: 'https://app-resources-luojigou.luojigou.vip/FnahepHJN1cSRD03VEFLebxL5Br-',
   trumptDog: "https://app-resources-luojigou.luojigou.vip/Fgh59QZy8NO8qG9f8sXMU4Xv4SKW",
-  coinTotal
+  coinTotal,
+  coinAni,
+  successFlag
 }
 
 

+ 1 - 0
vite.config.ts

@@ -4,4 +4,5 @@ import uni from "@dcloudio/vite-plugin-uni";
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [uni()],
+  base: './'
 });