123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698 |
- <template>
- <view class="luojigou-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="emits('playAudio')">
- <!-- <image class="dog" :src="staticImg.trumptDog" /> -->
- <image
- class="trumpt-icon dog"
- :src="props.playLoading ? staticImg.trumptGif : staticImg.trumptPng"
- />
- </view>
- <view class="tip " :key="props.cardDesc">
- <scroll-view
- scroll-y
- :style="{height: '88rpx',
- }"
- >
- {{props.cardDesc ? props.cardDesc : "加載中"}}
- </scroll-view>
- </view>
- <image
- class="luojigou-dog"
- :src="staticImg.luojigouDog"
- />
- </view>
- <view class="card" :style="{zIndex: viewZindex.quesView}" >
- <view class="ques" id="quesRef" >
- <image id="ques-url" :src="props.board.quesUrl" alt="" />
- </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="{
- top: Number(item.y) * rate + 'rpx',
- left: Number(item.x) * rate + 'rpx',
- width: 46 * rate + 'rpx',
- height: 46 * rate + 'rpx',
- backgroundColor: colorMap.get(item.color),
- scale: 0.7,
- borderRadius: '50%',
- opacity: props.tipsButton === 1 ? 1 : 0
- }"
- />
- </view> -->
- </view>
-
- <view class="ans" id="ansRef">
- <image :src="props.board.ansUrl" alt="" />
- </view>
- <movable-area
- v-if="props.cardType !== undefined"
- class="movable-area"
- id="movableAreaRef"
- :style="{
- width: 357 * rate+ 'rpx',
- height: 466 * rate + 'rpx',
- top: 102 * rate / adaptatio + 'rpx',
- left: 50 + '%',
- transform: `translateX(-${50}%)`,
- zIndex: viewZindex.moveView,
- scale: 1 / adaptatio
- }"
- >
- <movable-view
- v-for="item in buttons"
- :key="item.id"
- :disabled="item.disabled"
- :x="item.x "
- :y="item.y"
- damping="100"
- direction="all"
- class="movable-view"
- :style="{
- zIndex: item.zIndex,
- width: 46 * rate + 'rpx',
- height: 46 * rate+ 'rpx',
- }"
- @touchend="touchend($event, item)"
- @touchstart="touchStart(item)"
- >
- <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>
- </view>
- <!-- <audio :src="audioSrc" autoplay ></audio> -->
- </template>
- <script setup lang="ts" name="luojigou-board" >
- import { defineProps, reactive, ref, computed, getCurrentInstance, defineEmits, watch } from 'vue'
- import { useStaticImg, useSchedulerOnce, useQueryElInfo, useAdaptationIpadAndPhone } from '@/hooks/index'
- import type { CardModeEnum } from '@/enum/constant';
- import { useCalcQuantityStore } from '@/store/index';
- import { AudioController } from '@/controller/AudioController';
- import Hammer from 'hammerjs'
- import { onMounted } from 'vue';
- interface Buttons {
- id: number,
- x: number,
- y: number,
- initX: number,
- initY: number,
- url: string,
- color: API.Color,
- index: number,
- zIndex: number,
- ans: API.Color | null
- disabled: boolean
- }
- interface IProps {
- cardType: 0 | 1, // 0 四钮题卡 | 1 六钮题卡
- board: API.Board,
- mode: CardModeEnum,
- cardDesc: string,
- playLoading: boolean,
- getExpose: (records: any) => void
- tipsButton: number
- audioSrc: string
- }
- const staticImg = useStaticImg()
- const adaptatio = useAdaptationIpadAndPhone()
- const calcQuantityStore = useCalcQuantityStore()
- const props = defineProps<IProps>()
- const emits = defineEmits(['playAudio', 'submit'])
- const state = reactive({
- zIndex: 0, // 暂存zIndex
- scale: 1
- })
- const viewZindex = ref({
- moveView: 30,
- quesView: 31
- })
- // 手势操作题卡图片的state
- const hammerState = reactive({
- x: 0, // 记录水平方向上的偏移量
- y: 0, // 记录垂直方向上的偏移量
- scaleCount: 2,
- scaleIndex: 1
- })
- const colorMap = new Map([
- ['red', '#FF0002'],
- ['blue', '#0061B4'],
- ['purple', '#A764AB'],
- ['yellow', '#FFD800'],
- ['green', '#008D46'],
- ['orange', '#FF7F00'],
- ])
- const boardUrl = props.cardType == 1 ? staticImg.boardSix : staticImg.boardFour
- const ansRef = ref<UniApp.NodeInfo>()
- const quesRef = ref<UniApp.NodeInfo>()
- const movableAreaRef = ref<UniApp.NodeInfo>()
- useQueryElInfo('#quesRef', (nodeInfo) => quesRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
- useQueryElInfo('#ansRef', (nodeInfo) => ansRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
- useQueryElInfo('#movableAreaRef', (nodeInfo) => movableAreaRef.value = nodeInfo as UniApp.NodeInfo, getCurrentInstance()!)
- const baseRatio = adaptatio
- const rate = baseRatio * 2
- const { windowWidth, windowHeight } = uni.getSystemInfoSync()
- const unit = windowWidth / windowHeight > 0.6 ? 1 : 0.5
- // 几钮模板
- const copies = props.cardType === 0 ? 4 : 6
- const ansItemHeight = computed(() => Math.floor(ansRef.value?.height! / copies))
- const TPos = (x: number, y: number) => {
-
- return { x: x - movableAreaRef.value?.left! - 23 * rate * unit , y: y - movableAreaRef.value?.top! - 23 * rate * unit }
-
- }
- const getButtonIndex = (_y: number) => {
- console.log('ansItemHeight.value', ansItemHeight.value);
-
- return Math.floor(_y / ansItemHeight.value)
- }
- const getButtonPosByIndex = (index: number) => {
- console.log('getButtonPosByIndex:', ansRef.value?.width!, quesRef.value?.width! );
-
- const x = ansRef.value?.width! + quesRef.value?.width!
-
- const y = ansItemHeight.value * index + ansItemHeight.value / 2 - 23 * rate * unit
- return { x, y }
- }
- const buttonWidth = 46 * windowWidth / 375 * baseRatio
- const VSpace = props.cardType === 0 ?Math.floor(windowWidth / 375 * 26) * baseRatio + buttonWidth : Math.floor(windowWidth / 375 * 8) * baseRatio + buttonWidth
- const Y = 435 * rate
- const getX = (index: number) => index * VSpace + Math.floor(windowWidth / 375 * 11) * baseRatio
- // 注意一下四钮 用哪几个颜色的按钮
- let buttons = reactive<Buttons[]>([
- {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' },
- {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' },
- {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' },
- {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' },
- {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" },
- {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' },
- ])
- buttons.splice(props.cardType == 1 ? buttons.length : buttons.length - 2, buttons.length)
- // 让按钮回到原位
- const disPatchButtonGoInitPos = (index: number) => {
-
- useSchedulerOnce(() => {
- buttons[index].x = buttons[index].initX + Math.random()
- buttons[index].y = buttons[index].initY + Math.random()
- buttons[index].index = -1
- buttons[index].ans = null
- })
- }
- const touchStart = (item: Buttons) => {
- viewZindex.value.moveView = viewZindex.value.quesView + 1
- useSchedulerOnce(() => {
- state.zIndex ++
- item.zIndex = state.zIndex
- })
- }
- // 按钮脱手
- const touchend = (ev: TouchEvent, item: Buttons) => {
- if (item.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! ) {
- disPatchButtonGoInitPos(item.id)
- } else {
-
- const index = getButtonIndex(itemY <= 0 ? 0 : itemY)
- console.log('index:', index);
-
- const { x: targetX, y: targetY } = getButtonPosByIndex(index)
- console.log(item.x , targetX , item.y, targetY);
-
- // 直接点击答案区的按钮,答案区按钮回到初始位置
- if (item.x == targetX && item.y == targetY) {
- // disPatchButtonGoInitPos(item.id)
- } else {
- // 两个按钮都在答案区, 直接进行按钮之间的交换
- const isHasIndex = buttons.findIndex(button => button.index == index && button.id !== item.id )
-
- if (isHasIndex >= 0) { // 答案区的目标区域存在按钮
-
- if (item.index == -1) { // 答案区的目标区域存在按钮 操作按钮是从待操作区域移过来的 目标按钮回到原位
- // disPatchButtonGoInitPos(isHasIndex)
- } else { // 答案区的目标区域存在按钮 操作按钮是和目标按钮互换位置
- // useSchedulerOnce(() => {
- // const oldPos = getButtonPosByIndex(item.index)
- // buttons[isHasIndex].x = oldPos.x
- // buttons[isHasIndex].y = oldPos.y
- // buttons[isHasIndex].index = item.index
- // })
- }
- }
- // 将按钮放置到答案区对应的位置
- useSchedulerOnce(() => {
- item.x = targetX
- item.y = targetY
- item.index = index
- })
- checkAns(index, item)
- }
- }
- // 每次松手后把题卡的zIndex提高1
- viewZindex.value.quesView = viewZindex.value.moveView + 1
- }
- // 判断答案 (学习计划适用逻辑)
- const checkAns = (index: number, itemData: Buttons) => {
- const targetData = props.board.ansList
- console.log(targetData, index, itemData);
-
- // 移动到了正确位置
- if (targetData[index].ans === itemData.color) {
- itemData.ans = targetData[index].color
-
- calcQuantityStore.correctQuantity++
- AudioController.playCorrect()
- itemData.disabled = true
- } else { // 移动到了錯誤位置
- AudioController.playWrong()
- calcQuantityStore.wrongCount++
- itemData.disabled = true
- itemData.ans = null
- const rockNode = document.getElementById(`rock-id-${itemData.id}`)
- rockNode?.classList.add('rock-button-ani')
- useSchedulerOnce(() => {
- disPatchButtonGoInitPos(itemData.id)
- rockNode?.classList.remove('rock-button-ani')
- itemData.disabled = false
- }, 1000)
- }
-
- calcQuantityStore.totalQuantity++
- // 全部正确后触发提交答案的emit
- const ansLen = buttons.filter( button => button.ans).length
- if ( ansLen === ( props.cardType === 0 ? 4 : 6 )) {
- emits('submit')
- }
- }
- // 让题卡图片支持双指捏合
- const createHammer = () => {
- var square = document.querySelector('#ques-url');
- var hammer = new Hammer(square);
- hammer.get('pinch').set({ enable: true });
-
- hammer.on('pinchmove pinchstart pinchin pinchout', (e) => {
- if (e.type == "pinchstart") {
- hammerState.scaleIndex = hammerState.scaleCount || 1
- }
- if (hammerState.scaleIndex * e.scale <= 1) {
- hammerState.scaleCount = 1
- hammerState.scaleIndex = 1
- return
- }
-
- hammerState.scaleCount = hammerState.scaleIndex * e.scale;
- console.log(hammerState.scaleCount);
-
- document.getElementById('quesRef')!.style.transform = `scale(${hammerState.scaleIndex * e.scale})`
- })
- hammer.on('panright panleft panup pandown', (e) => {
- document.getElementById('quesRef')!.style.transform = "translateX(" + (e.deltaX + hammerState.x) + "px)" + "translateY(" + (e.deltaY + hammerState.y) + "px)" + "scale(" + (hammerState.scaleCount) + ")";
- });
- hammer.on('doubletap', (e) => {
- hammerState.x = 0;
- hammerState.y = 0;
- hammerState.scaleCount = 1; // 重置缩放比例为1
- document.getElementById('quesRef')!.style.transform = "translateX(0px) translateY(0px) scale(1)"; // 重置位置和缩放效果
- });
- hammer.on('panend', (e) => {
- hammerState.x = e.deltaX + hammerState.x; // 记录水平方向上的偏移量
- hammerState.y = e.deltaY + hammerState.y; // 记录垂直方向上的偏移量
- console.log(hammerState.x);
-
- const node = document.getElementById('quesRef')
-
- if (hammerState.scaleCount <= 1) {
-
- node!.style.transform = `translateX(0px) translateY(0px) scale(1)`
- hammerState.x = 0
- hammerState.y = 0
- hammerState.scaleCount = 1
- hammerState.scaleIndex = 1
- } else {
- detectionEdge(0, 0)
- }
- });
- // 放大后拖拽露出边缘后, 回弹的判定函数 回弹条件是当前放大的倍数 * 题卡的(宽/高)
- const detectionEdge = (x: number, y: number) => {
- const node = document.getElementById('quesRef')
- const top = (quesRef.value?.height! * hammerState.scaleCount - quesRef.value?.height!) / 2
- const bottom = -top
- const left = (quesRef.value?.width! * hammerState.scaleCount - quesRef.value?.width!) / 2
- const right = -left
-
- if ( Math.abs(hammerState.y) > top) {
- hammerState.y = hammerState.y > 0 ? top: bottom
- }
- if ( Math.abs(hammerState.x) > left) {
- hammerState.x = hammerState.x > 0 ? left: right
- }
- node!.style.transform = `translateX(${hammerState.x}px) translateY(${hammerState.y}px) scale(${hammerState.scaleCount})`
-
- }
- }
- onMounted(() => {
- createHammer()
- })
- </script>
- <style lang="less" scoped >
- .luojigou-board {
- width: 714rpx;
- height: 1234rpx;
- position: relative;
- display: flex;
- justify-content: center;
- .board {
- width: 714rpx;
- height: 1234rpx;
- position: relative;
- .board-img {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- z-index: 1;
- }
- .board-header {
- width: 100%;
- height: 148rpx;
- position: relative;
- top: 7rpx;
- z-index: 12;
- .trumpt {
- width: 96rpx;
- height: 96rpx;
- position: relative;
- top: 44rpx;
- left: 54rpx;
- .dog {
- width: 96rpx;
- height: 96rpx;
- position: absolute;
- z-index: 1;
- }
- .trumpt-icon {
- // width: 40rpx;
- // height: 40rpx;
- position: absolute;
- z-index: 2;
- right: -12rpx;
- bottom: -6rpx;
- }
- }
- .tip {
- width: 490rpx;
- height: 72rpx;
- font-size: 24rpx;
- font-family: PingFangSC-Medium, PingFang SC;
- font-weight: 500;
- color: #ffffff;
- position: absolute;
- top: 50rpx;
- left: 176rpx;
- }
- .luojigou-dog {
- width: 84rpx;
- height: 84rpx;
- position: absolute;
- right: 46rpx;
- top: -48rpx;
- }
- }
- .card {
- position: absolute;
- top: 206rpx;
- left: 26rpx;
- z-index: 2;
- width: 450rpx;
- height: 772rpx;
- overflow: hidden;
- border-top-left-radius: 40rpx;
- border-bottom-left-radius: 40rpx;
- }
- .ques {
- width: 450rpx;
- height: 772rpx;
- border-right: 2rpx solid #006CAA;
- border-top-left-radius: 40rpx;
- border-bottom-left-radius: 40rpx;
- overflow: hidden;
- touch-action: none !important;
- scale: 1;
- box-sizing: border-box;
- image {
- width: 100%;
- height: 100%;
- display: block;
- // border-top-left-radius: 40rpx;
- // border-bottom-left-radius: 40rpx;
- }
- }
- .ans {
- width: 150rpx;
- height: 772rpx;
- display: flex;
- flex-direction: column;
- border-top-right-radius: 40rpx;
- border-bottom-right-radius: 40rpx;
- overflow: hidden;
- background-color: #fff;
- position: absolute;
- top: 206rpx;
- left: 476rpx;
- z-index: 2;
- image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- }
- .ans .ans-item:last-child {
- border-bottom: none
- }
- .buttons {
- position: relative;
- top: 200rpx;
- left: 20rpx;
- width: 680rpx;
- height: 1000rpx;
- image {
- position: absolute;
- left: 0;
- top: 0;
- }
- }
- .movable-area {
- width: 714rpx;
- height: 984rpx;
- background-color: transparent;
- position: absolute;
- top: 192rpx;
- z-index: 30;
- touch-action: none;
- left: 0px;
- -moz-transform: none;
- -webkit-transform: none;
- -o-transform: none;
- -ms-transform: none;
- transform: none;
- transform-origin: 0% 0%;
- .movable-view {
- width: 92rpx;
- height: 92rpx;
- touch-action: none;
- .movable-image {
- 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;
- }
- }
- }
- }
- .mark-button {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0px;
- left: 0px;
- z-index: 29;
- .movable-image {
- position: absolute;
- width: 92rpx;
- height: 92rpx;
- display: block;
- }
- }
-
- .opra {
- width: 100vw;
- height: 1216rpx;
- background: url('../../assets/boardBg.png');
- display: flex;
- justify-content: center;
- 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;
- }
- }
- /deep/ .uni-scroll-view-content {
- display: flex;
- align-items: center;
- }
- </style>
|