luojigou-board.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <template>
  2. <view class="luojigou-board" >
  3. <view class="board" :style="{scale: adaptatio, transformOrigin: '50% 0%'}" >
  4. <image class="board-img" :src="boardUrl" />
  5. <view class="board-header" id="game-label">
  6. <view class="trumpt" @click="emits('playAudio')">
  7. <image class="dog" :src="staticImg.trumptDog" />
  8. <image
  9. class="trumpt-icon"
  10. :src="props.playLoading ? staticImg.trumptGif : staticImg.trumptPng"
  11. />
  12. </view>
  13. <view class="tip " :key="props.cardDesc">
  14. <scroll-view
  15. scroll-y
  16. :style="{height: '88rpx',
  17. }"
  18. >
  19. {{props.cardDesc ? props.cardDesc : "加載中"}}
  20. </scroll-view>
  21. </view>
  22. </view>
  23. <view class="card" >
  24. <view class="ques" ref="quesRef" id="quesRef" >
  25. <image :src="props.board.quesUrl" alt="" />
  26. </view>
  27. <view class="ans" ref="ansRef" id="ansRef">
  28. <image :src="props.board.ansUrl" alt="" />
  29. </view>
  30. </view>
  31. </view>
  32. <view
  33. class="mark-button"
  34. :style="{
  35. width: 357 * rate + 'rpx',
  36. height: 466 * rate + 'rpx',
  37. top: 102 * rate + 'rpx',
  38. left: '50%',
  39. transform: 'translateX(-50%)'
  40. }"
  41. >
  42. <view
  43. v-for="item in props.board.buttons"
  44. class="movable-image"
  45. :style="{
  46. top: Number(item.y) * rate + 'rpx',
  47. left: Number(item.x) * rate + 'rpx',
  48. width: 46 * rate + 'rpx',
  49. height: 46 * rate + 'rpx',
  50. backgroundColor: colorMap.get(item.color),
  51. scale: 0.7,
  52. borderRadius: '50%'
  53. }"
  54. />
  55. </view>
  56. <movable-area
  57. v-if="props.cardType !== undefined"
  58. class="movable-area"
  59. id="movableAreaRef"
  60. :style="{
  61. width: 357 * rate + 'rpx',
  62. height: 466 * rate + 'rpx',
  63. top: 102 * rate + 'rpx',
  64. left: '50%',
  65. transform: 'translateX(-50%)'
  66. }"
  67. >
  68. <movable-view
  69. v-for="item in buttons"
  70. :key="item.id"
  71. :disabled="item.disabled"
  72. :x="item.x"
  73. :y="item.y"
  74. damping="100"
  75. direction="all"
  76. class="movable-view"
  77. :style="{
  78. zIndex: item.zIndex,
  79. width: 46 * rate + 'rpx',
  80. height: 46 * rate + 'rpx',
  81. }"
  82. @touchend="touchend($event, item)"
  83. @touchstart="touchStart(item)"
  84. >
  85. <image
  86. :id="`rock-id-${item.id}`"
  87. :class="`movable-image `"
  88. :style="{willChange: 'transform', transformOrigin: `center bottom`}"
  89. :src="item.url"
  90. />
  91. <image
  92. v-if="item.ans"
  93. class="success-flag"
  94. :src="staticImg.successFlag"
  95. />
  96. </movable-view>
  97. </movable-area>
  98. </view>
  99. </template>
  100. <script setup lang="ts" name="luojigou-board" >
  101. import { defineProps, reactive, ref, computed, getCurrentInstance, defineEmits } from 'vue'
  102. import { useStaticImg, useSchedulerOnce, useQueryElInfo, useAdaptationIpadAndPhone } from '@/hooks/index'
  103. import type { CardModeEnum } from '@/enum/constant';
  104. import { useCalcQuantityStore } from '@/store/index';
  105. import { AudioController } from '@/controller/AudioController';
  106. interface Buttons {
  107. id: number,
  108. x: number,
  109. y: number,
  110. initX: number,
  111. initY: number,
  112. url: string,
  113. color: API.Color,
  114. index: number,
  115. zIndex: number,
  116. ans: API.Color | null
  117. disabled: boolean
  118. }
  119. interface IProps {
  120. cardType: 0 | 1, // 0 四钮题卡 | 1 六钮题卡
  121. board: API.Board,
  122. mode: CardModeEnum,
  123. cardDesc: string,
  124. playLoading: boolean,
  125. getExpose: (records: any) => void
  126. }
  127. const staticImg = useStaticImg()
  128. const adaptatio = useAdaptationIpadAndPhone()
  129. const calcQuantityStore = useCalcQuantityStore()
  130. const props = defineProps<IProps>()
  131. const emits = defineEmits(['playAudio', 'submit'])
  132. const state = reactive({
  133. zIndex: 0, // 暂存zIndex
  134. disabled: false, // 是否禁用moveview
  135. correctQuantity: 0, // 正确数量()
  136. totalQuantity: 0 // 总数量 (移动按钮数量)
  137. })
  138. const colorMap = new Map([
  139. ['red', '#FF0002'],
  140. ['blue', '#0061B4'],
  141. ['purple', '#A764AB'],
  142. ['yellow', '#FFD800'],
  143. ['green', '#008D46'],
  144. ['orange', '#FF7F00'],
  145. ])
  146. const boardUrl = props.cardType == 1 ? staticImg.boardSix : staticImg.boardFour
  147. const ansRef = ref<UniApp.NodeInfo>()
  148. const quesRef = ref<UniApp.NodeInfo>()
  149. const movableAreaRef = ref<UniApp.NodeInfo>()
  150. useQueryElInfo('#quesRef', (nodeInfo) => quesRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
  151. useQueryElInfo('#ansRef', (nodeInfo) => ansRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
  152. useQueryElInfo('#movableAreaRef', (nodeInfo) => movableAreaRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
  153. const baseRatio = adaptatio
  154. const rate = baseRatio * 2
  155. const { windowWidth, windowHeight } = uni.getSystemInfoSync()
  156. const unit = windowWidth / windowHeight > 0.6 ? 1 : 0.5
  157. const ansItemHeight = computed(() => Math.floor(ansRef.value?.height! / copies))
  158. const TPos = (x: number, y: number) => {
  159. return { x: x - movableAreaRef.value?.left! - 23 * rate * unit , y: y - movableAreaRef.value?.top! - 23 * rate * unit }
  160. }
  161. const getButtonIndex = (_y: number) => Math.floor(_y / ansItemHeight.value)
  162. const getButtonPosByIndex = (index: number) => {
  163. const x = ansRef.value?.width! + quesRef.value?.width!
  164. const y = ansItemHeight.value * index + ansItemHeight.value / 2 - 23 * rate * unit
  165. return { x, y }
  166. }
  167. // 几钮模板
  168. const copies = props.cardType === 0 ? 4 : 6
  169. const buttonWidth = 46 * windowWidth / 375 * baseRatio
  170. const VSpace = props.cardType === 0 ?Math.floor(windowWidth / 375 * 26) * baseRatio + buttonWidth : Math.floor(windowWidth / 375 * 8) * baseRatio + buttonWidth
  171. const Y = 435 * rate
  172. const getX = (index: number) => index * VSpace + Math.floor(windowWidth / 375 * 11) * baseRatio
  173. // 注意一下四钮 用哪几个颜色的按钮
  174. let buttons = reactive<Buttons[]>([
  175. {id: 0, ans: null, x: getX(0), y: Y * rate,disabled: false, initX: getX(0), initY: Y * rate, zIndex: 0, index: -1, url: staticImg.red, color: 'red' },
  176. {id: 1, ans: null, x: getX(1), y: Y * rate,disabled: false, initX: getX(1), initY: Y * rate, zIndex: 0, index: -1, url: staticImg.blue, color: 'blue' },
  177. {id: 2, ans: null, x: getX(2), y: Y * rate,disabled: false, initX: getX(2), initY: Y * rate, zIndex: 0, index: -1, url: staticImg.green, color: 'green' },
  178. {id: 3, ans: null, x: getX(3), y: Y * rate,disabled: false, initX: getX(3), initY: Y * rate, zIndex: 0, index: -1, url: staticImg.orange, color: 'orange' },
  179. {id: 4, ans: null, x: getX(4), y: Y * rate,disabled: false, initX: getX(4), initY: Y * rate, zIndex: 0, index: -1, url: staticImg.yellow, color: "yellow" },
  180. {id: 5, ans: null, x: getX(5), y: Y * rate,disabled: false, initX: getX(5), initY: Y * rate, zIndex: 0, index: -1, url: staticImg.purple, color: 'purple' },
  181. ])
  182. buttons.splice(props.cardType == 1 ? buttons.length : buttons.length - 2, buttons.length)
  183. // 让按钮回到原位
  184. const disPatchButtonGoInitPos = (index: number) => {
  185. useSchedulerOnce(() => {
  186. buttons[index].x = buttons[index].initX + Math.random()
  187. buttons[index].y = buttons[index].initY + Math.random()
  188. buttons[index].index = -1
  189. buttons[index].ans = null
  190. })
  191. useSchedulerOnce(() => {
  192. state.disabled = false
  193. }, 200)
  194. }
  195. const touchStart = (item: Buttons) => {
  196. useSchedulerOnce(() => {
  197. state.zIndex ++
  198. item.zIndex = state.zIndex
  199. })
  200. }
  201. // 按钮脱手
  202. const touchend = (ev: TouchEvent, item: Buttons) => {
  203. if (item.disabled) return
  204. const { x: itemX, y: itemY } = TPos(ev.changedTouches[0].pageX, ev.changedTouches[0].pageY)
  205. // 返回原点 (没有放在答案区, 按钮回到初始位置)
  206. if (itemX < quesRef.value?.width! || itemY > ansRef.value?.height! ) {
  207. disPatchButtonGoInitPos(item.id)
  208. } else {
  209. const index = getButtonIndex(itemY <= 0 ? 0 : itemY)
  210. const { x: targetX, y: targetY } = getButtonPosByIndex(index)
  211. console.log(item.x , targetX , item.y, targetY);
  212. // 直接点击答案区的按钮,答案区按钮回到初始位置
  213. if (item.x == targetX && item.y == targetY) {
  214. // disPatchButtonGoInitPos(item.id)
  215. } else {
  216. // 两个按钮都在答案区, 直接进行按钮之间的交换
  217. const isHasIndex = buttons.findIndex(button => button.index == index && button.id !== item.id )
  218. if (isHasIndex >= 0) { // 答案区的目标区域存在按钮
  219. if (item.index == -1) { // 答案区的目标区域存在按钮 操作按钮是从待操作区域移过来的 目标按钮回到原位
  220. // disPatchButtonGoInitPos(isHasIndex)
  221. } else { // 答案区的目标区域存在按钮 操作按钮是和目标按钮互换位置
  222. // useSchedulerOnce(() => {
  223. // const oldPos = getButtonPosByIndex(item.index)
  224. // buttons[isHasIndex].x = oldPos.x
  225. // buttons[isHasIndex].y = oldPos.y
  226. // buttons[isHasIndex].index = item.index
  227. // })
  228. }
  229. }
  230. // 将按钮放置到答案区对应的位置
  231. useSchedulerOnce(() => {
  232. item.x = targetX
  233. item.y = targetY
  234. item.index = index
  235. })
  236. checkAns(index, item)
  237. }
  238. }
  239. }
  240. // 判断答案 (学习计划适用逻辑)
  241. const checkAns = (index: number, itemData: Buttons) => {
  242. const targetData = props.board.ansList
  243. console.log(targetData, index, itemData);
  244. // 移动到了正确位置
  245. if (targetData[index].ans === itemData.color) {
  246. itemData.ans = targetData[index].color
  247. calcQuantityStore.correctQuantity++
  248. AudioController.playCorrect()
  249. itemData.disabled = true
  250. } else { // 移动到了錯誤位置
  251. AudioController.playWrong()
  252. calcQuantityStore.wrongCount++
  253. itemData.ans = null
  254. const rockNode = document.getElementById(`rock-id-${itemData.id}`)
  255. rockNode?.classList.add('rock-button-ani')
  256. useSchedulerOnce(() => {
  257. disPatchButtonGoInitPos(itemData.id)
  258. rockNode?.classList.remove('rock-button-ani')
  259. }, 1000)
  260. }
  261. calcQuantityStore.totalQuantity++
  262. // 全部正确后触发提交答案的emit
  263. const ansLen = buttons.filter( button => button.ans).length
  264. if ( ansLen === ( props.cardType === 0 ? 4 : 6 )) {
  265. emits('submit')
  266. }
  267. }
  268. </script>
  269. <style lang="less" scoped >
  270. .luojigou-board {
  271. width: 714rpx;
  272. height: 1234rpx;
  273. position: relative;
  274. display: flex;
  275. justify-content: center;
  276. .board {
  277. width: 714rpx;
  278. height: 1234rpx;
  279. position: relative;
  280. .board-img {
  281. width: 100%;
  282. height: 100%;
  283. position: absolute;
  284. top: 0;
  285. left: 0;
  286. z-index: 1;
  287. }
  288. .board-header {
  289. width: 100%;
  290. height: 148rpx;
  291. position: relative;
  292. top: 7rpx;
  293. z-index: 12;
  294. .trumpt {
  295. width: 96rpx;
  296. height: 96rpx;
  297. position: relative;
  298. top: 44rpx;
  299. left: 54rpx;
  300. .dog {
  301. width: 96rpx;
  302. height: 96rpx;
  303. position: absolute;
  304. z-index: 1;
  305. }
  306. .trumpt-icon {
  307. width: 40rpx;
  308. height: 40rpx;
  309. position: absolute;
  310. z-index: 2;
  311. right: -12rpx;
  312. bottom: -6rpx;
  313. }
  314. }
  315. .tip {
  316. width: 490rpx;
  317. height: 72rpx;
  318. font-size: 24rpx;
  319. font-family: PingFangSC-Medium, PingFang SC;
  320. font-weight: 500;
  321. color: #ffffff;
  322. position: absolute;
  323. top: 50rpx;
  324. left: 176rpx;
  325. }
  326. }
  327. .card {
  328. width: 606rpx;
  329. height: 772rpx;
  330. border-radius: 40rpx;
  331. overflow: hidden;
  332. display: flex;
  333. justify-content: space-between;
  334. position: absolute;
  335. top: 206rpx;
  336. left: 26rpx;
  337. z-index: 2;
  338. }
  339. .ques {
  340. width: 450rpx;
  341. height: 772rpx;
  342. border-right: 2rpx solid #006CAA;
  343. image {
  344. width: 100%;
  345. height: 100%;
  346. display: block;
  347. }
  348. }
  349. .ans {
  350. width: 170rpx;
  351. height: 100%;
  352. display: flex;
  353. flex-direction: column;
  354. border-top-right-radius: 40rpx;
  355. border-bottom-right-radius: 40rpx;
  356. overflow: hidden;
  357. background-color: #fff;
  358. image {
  359. width: 100%;
  360. height: 100%;
  361. object-fit: cover;
  362. }
  363. }
  364. .ans .ans-item:last-child {
  365. border-bottom: none
  366. }
  367. .buttons {
  368. position: relative;
  369. top: 200rpx;
  370. left: 20rpx;
  371. width: 680rpx;
  372. height: 1000rpx;
  373. image {
  374. position: absolute;
  375. left: 0;
  376. top: 0;
  377. }
  378. }
  379. }
  380. .mark-button {
  381. width: 100%;
  382. height: 100%;
  383. position: absolute;
  384. top: 0px;
  385. left: 0px;
  386. z-index: 29;
  387. .movable-image {
  388. position: absolute;
  389. width: 92rpx;
  390. height: 92rpx;
  391. display: block;
  392. }
  393. }
  394. .movable-area {
  395. width: 714rpx;
  396. height: 984rpx;
  397. background-color: transparent;
  398. position: absolute;
  399. top: 192rpx;
  400. left: 50%;
  401. transform: translateX(-50%);
  402. z-index: 30;
  403. touch-action: none;
  404. left: 0px;
  405. -moz-transform: none;
  406. -webkit-transform: none;
  407. -o-transform: none;
  408. -ms-transform: none;
  409. transform: none;
  410. .movable-view {
  411. width: 92rpx;
  412. height: 92rpx;
  413. touch-action: none;
  414. .movable-image {
  415. width: 100%;
  416. height: 100%;
  417. transition: width 0.2s;
  418. z-index: 1;
  419. }
  420. .success-flag {
  421. width: 36rpx;
  422. height: 26rpx;
  423. display: block;
  424. position: absolute;
  425. top: 50%;
  426. left: 50%;
  427. transform: translate(-50%, -50%);
  428. z-index: 2;
  429. }
  430. }
  431. }
  432. .opra {
  433. width: 100vw;
  434. height: 1216rpx;
  435. background: url('../../assets/boardBg.png');
  436. display: flex;
  437. justify-content: center;
  438. padding-top: 46rpx;
  439. }
  440. }
  441. .rock-button-ani {
  442. animation: rock 1s;
  443. }
  444. @keyframes rock {
  445. 0% {
  446. rotate: 0deg;
  447. }
  448. 25% {
  449. rotate: -10deg;
  450. }
  451. 50% {
  452. rotate: 10deg;
  453. }
  454. 75% {
  455. rotate: -10deg;
  456. }
  457. 100% {
  458. rotate: 0deg;
  459. }
  460. }
  461. /deep/ .uni-scroll-view-content {
  462. display: flex;
  463. align-items: center;
  464. }
  465. </style>