微信小程序找不同游戏(有效果图)

效果图


index.vue

复制代码
<template>
  <wxy-nav-back :isOccupy="false" des="找不同" />
  <head-info :refresh="refresh" :countdown="countdown" :title="levelTitle" :progress="correct.length" :len="differences.length" />
  <img-info v-if="list.length" ref="imgInfoRef" :list="list" :differences="differences" :tipsShow="tipsShow"
            :correct="correct" @change="updateNum" />
  <view class="wxy-button flex-around">
    <button class="flex-center btn wxy-btn bg-main" :disabled="tipsNum === 0 || tipsShow"
            @click="goTips">
      提示({{ tipsNum }})
    </button>
    <button class="bg-grey flex-center btn wxy-btn" @click="reset">
      重置
    </button>
  </view>
  <view style="height: 9vh;" />
  <victory-model :show="victoryShow" @change="getData(true)" />
</template>

<script setup lang='ts'>
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ref, onMounted } from 'vue'
import { throttle } from '@/modules/tool'
import headInfo from './components/head-info/head-info.vue'
import imgInfo from './components/img-info/img-info.vue'
import victoryModel from './components/victory-model/victory-model.vue'
import { useData } from './hooks/data'

const imgInfoRef = ref()

const {
  refresh,
  level,
  levelTitle,
  list,
  differences,
  correct,
  tipsShow,
  tipsNum,
  victoryShow,
  countdown,
  data,
} = useData()

let timer:any
/** 倒计时 */
const startCountdown = () => {
  timer = setInterval(() => {
    if (countdown.value > 0) countdown.value--
    else {
      clearInterval(timer)
      victoryShow.value = 2
    }
  }, 1000)
}

/** 去提示 */
const goTips = () => {
  tipsNum.value -= 1
  tipsShow.value = true
}

/** 更新进度 */
const updateNum = (val:{x:number, y:number}) => {
  if (tipsShow.value) tipsShow.value = false
  correct.value.push(val)
  if (correct.value.length === differences.value.length) {
    clearInterval(timer)
    victoryShow.value = 1
  }
}

/** 重置 */
const reset = throttle(() => {
  correct.value = []
  tipsNum.value = 3
  tipsShow.value = false
  countdown.value = 120
  victoryShow.value = 0
  imgInfoRef.value.setAnswer()
})

/** 获取数据 */
const getData = (status?:boolean) => {
  refresh.value = true
  if (status) {
    if (victoryShow.value === 1) level.value += 1
    if (data.length - 1 < level.value) level.value = 0
    list.value = []
    reset()
  }
  setTimeout(() => {
    const o = data[level.value]
    levelTitle.value = o.title
    list.value = o.list
    differences.value = o.differences
    refresh.value = false
    startCountdown()
  }, 100)
}

onMounted(() => {
  getData()
})
</script>

<style lang='scss' scoped>
.btn{
  min-width: 200rpx;
  height: 80rpx;
  padding: 0 30rpx;
  font-weight: 700;
  border-radius: 20rpx;
}
</style>

head-info.vue

复制代码
<template>
  <view class="nav wdh bg-gradual-blue padding-md margin-top-sm">
    <view class="nav-title fade-in-right-70 animated" style="animation-delay:.2s;"
          :style="refresh?`visibility: visible;animation-name:none;`:''">
      {{ title }}
    </view>
    <view class="flex-around margin-top-md fade-in-up-50 animated" style="animation-delay:.3s;"
          :style="refresh?`visibility: visible;animation-name:none;`:''">
      <view class="nav-content flex-center">
        <view>
          <view class="flex-center nav-content-title">
            <view class="cu-icon-star nav-content-icon" />
            <view>{{ progress }}/{{ len }}</view>
          </view>
          <view class="nav-content-text">已找到</view>
        </view>
      </view>
      <view class="nav-content flex-center margin-left-md">
        <view>
          <view class="flex-center nav-content-title">
            <view class="cu-icon-time nav-content-icon" />
            <view>{{ countdown }}</view>
          </view>
          <view class="nav-content-text">剩余时间</view>
        </view>
      </view>
    </view>
    <view class="nav-line fade-in animated" style="animation-delay:.5s;"
          :style="refresh?`visibility: visible;animation-name:none;`:`--w:${(progress / len) * 100}%;`" />
  </view>
</template>

<script setup lang='ts'>
defineProps({
  progress: {
    type: Number,
    default: 0,
  },
  len: {
    type: Number,
    default: 0,
  },
  refresh: {
    type: Boolean,
    default: false,
  },
  title: {
    type: String,
    default: '',
  },
  countdown: {
    type: Number,
    default: 120,
  },
})
</script>

<style lang='scss' scoped>
.nav{
  text-align: center;
  border-radius: 20rpx;

  &-title{
    font-weight: 700;
    font-size: 48rpx;
  }

  &-line{
    position: relative;
    width: 100%;
    height: 20rpx;
    margin-top: 30rpx;
    overflow: hidden;
    background-color: rgb(255 255 255 / 50%);
    border-radius: 20rpx;
    backdrop-filter: blur(10px) brightness(90%);
  }

  &-line::after{
    position: absolute;
    top: 0;
    left: 0;
    width: var(--w);
    height: 100%;
    background: var(--main);
    border-radius: 20rpx;
    transition: all .3s;
    content: '';
  }

  &-content{
    flex: 1;
    height: 120rpx;
    background-color: rgb(255 255 255 / 50%);
    border-radius: 20rpx;
    backdrop-filter: blur(10px) brightness(90%);

    .cu-icon-star{
      color: var(--main);
    }

    &-icon{
      margin-right: 6rpx;
      font-size: 36rpx;
    }

    &-title{
      font-weight: 700;
      font-size: 36rpx;
    }

    &-text{
      margin-top: 6rpx;
      color: rgb(255 255 255 / 90%);
      font-size: 24rpx;
    }
  }
}
</style>

img-info.vue

复制代码
<template>
  <view v-for="(item,index) in list" :id="`container${index}`" :key="index"
        class="content fade-in animated" style="animation-delay:.5s;"
        @click="getLocation($event,index)">
    <image class="content-img" :src="item" />
    <!-- 提示框 -->
    <view v-if="answer.length && tipsShow" class="content-tips" :style="`--x:${answer[0].x}px;--y:${answer[0].y}px;`" />
    <!-- 正确列表 -->
    <view v-for="(i,iIndex) in correct" :key="iIndex"
          class="content-box flex-center content-success" :style="`--x:${i.x}px;--y:${i.y}px;`">
      <view class="text-green cu-icon-adopt content-icon" />
      <particle-burst />
    </view>
  </view>
  <!-- 错误列表 -->
  <view v-for="(item,index) in errorArr" :key="index"
        class="content-box flex-center content-hide"
        :class="{'content-show':item.show}"
        :style="`--x:${item.x}px;--y:${item.y}px;`">
    <view class="text-red cu-icon-fail content-icon" />
  </view>
</template>

<script setup lang='ts'>
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ref, onMounted, getCurrentInstance } from 'vue'
import particleBurst from '../particle-burst/particle-burst.vue'

const emit = defineEmits(['change'])

const props = defineProps({
  list: {
    type: Array as () => any[],
    default: () => [],
  },
  differences: {
    type: Array as () => any[],
    default: () => [],
  },
  correct: {
    type: Array as () => any[],
    default: () => [],
  },
  tipsShow: {
    type: Boolean,
    default: false,
  },
})

/** 答案列表 */
const answer = ref<any[]>([])
/** 错误列表 */
const errorArr = ref([
  { x: 0, y: 0, show: false },
  { x: 0, y: 0, show: false },
  { x: 0, y: 0, show: false },
  { x: 0, y: 0, show: false },
  { x: 0, y: 0, show: false },
  { x: 0, y: 0, show: false },
])
/** 错误下标 */
let errorIndex = 0

/** 容器坐标 */
const relative = [
  { x: 0, y: 0 },
  { x: 0, y: 0 },
]

/** 设置答案 */
const setAnswer = () => {
  answer.value = props.differences.map((item) => ({
    x: item.x - 25,
    y: item.y - 25,
    w: 50,
    h: 50,
  }))
}

/** 验证答案 */
const isInAnswerArea = (clickX: number, clickY: number) => {
  const hitAnswer = answer.value.findIndex((item) => (
    clickX >= item.x
    && clickX <= item.x + item.w
    && clickY >= item.y
    && clickY <= item.y + item.h
  ))
  return hitAnswer
}
/** 获取点击坐标 */
const getLocation = (e:any, index:number) => {
  const o = e.detail
  const d = relative[index]
  const clickX = o.x - d.x
  const clickY = o.y - d.y
  const i = isInAnswerArea(clickX, clickY)
  if (i >= 0) {
    const val = answer.value[i]
    emit('change', { x: val.x, y: val.y })
    answer.value.splice(i, 1)
    return
  }
  const v = errorIndex
  errorIndex = errorIndex + 1 === errorArr.value.length ? 0 : errorIndex + 1
  errorArr.value[v] = {
    x: o.x - 25,
    y: o.y - 25,
    show: true,
  }
  setTimeout(() => {
    errorArr.value[v].show = false
  }, 1000)
}
const instance = getCurrentInstance()
/** 获取容器坐标 */
const getHeight = () => {
  for (let i = 0; i < relative.length; i++) {
    uni.createSelectorQuery().in(instance?.proxy)
      .select(`#container${i}`)
      .boundingClientRect((res:any) => {
        relative[i] = {
          x: parseInt(res.left),
          y: parseInt(res.top),
        }
      }).exec()
  }
}

onMounted(() => {
  setAnswer()
  getHeight()
})

defineExpose({
  setAnswer,
})
</script>

<style lang='scss' scoped>
@keyframes magnify{
  from{
    transform: scale(1);
    opacity: 1;
  }

  to{
    transform: scale(2);
    opacity: 0;
  }
}

.content{
  position: relative;
  width: 360px;
  height: 220px;
  margin: 20rpx auto 0;
  overflow: hidden;
  border-radius: 20rpx;

  &-img{
    position: absolute;
    width: 100%;
    height: 100%;
  }

  &-tips{
    --w:50px;

    position: absolute;
    top: var(--y);
    left: var(--x);
    z-index: 1;
    width: var(--w);
    height: var(--w);
    background: rgb(234 179 8 / 20%);
    border: 3px solid var(--main);
    border-radius: 50%;
  }

  &-tips::after{
    position: absolute;
    top: -3px;
    left: -3px;
    width:100%;
    height: 100%;
    border: 3px solid var(--main);
    border-radius: 50%;
    animation: magnify 1s infinite;
    content: '';
  }
}

@keyframes enlarge{
  from{
    transform: scale(0.7);
    opacity: 0;
  }

  to{
    transform: scale(1);
    opacity: 1;
  }
}

.content-success{
  animation: enlarge .3s;
}

.content-box{
  position: absolute;
  top: var(--y);
  left: var(--x);
  z-index: 2;
  width: 50px;
  height: 50px;
  font-size: 88rpx;
}

.content-hide{
  transform: scale(0.7);
  opacity: 0;
  transition: transform .3s,opacity .3s;
  pointer-events: none;
}

.content-show{
  transform: scale(1);
  opacity: 1;
}

.content-icon{
  font-size: 88rpx;
}
</style>

particle-burst.vue

复制代码
<template>
  <view class="burst-container">
    <view class="particle" />
    <view class="particle" />
    <view class="particle" />
    <view class="particle" />
    <view class="particle" />
    <view class="particle" />
    <view class="particle" />
    <view class="particle" />
  </view>
</template>

<script setup lang='ts'>

</script>

<style lang='scss' scoped>
.burst-container {
  position: absolute;
  top:0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

.particle {
  position: absolute;
  width: 20rpx;
  height: 20rpx;
  background-color: var(--main);
  border-radius: 50%;
  opacity: 0;
}

@keyframes burst {
  0% {
    transform: scale(0.1);
    opacity: 0;
  }

  20% {
    opacity: 1;
  }

  100% {
    transform: scale(1.5) translate(var(--tx), var(--ty));
    opacity: 0;
  }
}

.particle:nth-child(1) {
  --tx: -25px;
  --ty: -25px;

  animation: burst .6s ease-out .1s forwards;
}

.particle:nth-child(2) {
  --tx: 25px;
  --ty: -25px;

  animation: burst .6s ease-out .1s forwards;
}

.particle:nth-child(3) {
  --tx: -25px;
  --ty: 25px;

  animation: burst .6s ease-out .2s forwards;
}

.particle:nth-child(4) {
  --tx: 25px;
  --ty: 25px;

  animation: burst .6s ease-out .2s forwards;
}

.particle:nth-child(5) {
  --tx: 0px;
  --ty: -50px;

  animation: burst .6s ease-out .3s forwards;
}

.particle:nth-child(6) {
  --tx: 0px;
  --ty: 50px;

  animation: burst .6s ease-out .3s forwards;
}

.particle:nth-child(7) {
  --tx: -50px;
  --ty: 0px;

  animation: burst .6s ease-out .4s forwards;
}

.particle:nth-child(8) {
  --tx: 50px;
  --ty: 0px;

  animation: burst .6s ease-out .4s forwards;
}
</style>

victory-model.vue

复制代码
<template>
  <!-- 成功 -->
  <view class="wxy-modal" :class="{'show':show === 1}">
    <view class="wxy-dialog" @click.stop>
      <view class="victory padding-xl bg-white">
        <image class="victory-img" src="https://get.hnzmsz.com/gw/victory.png" />
        <view class="victory-title">关卡完成</view>
        <view>太棒了!你找到了所有不同点</view>
        <view class="flex-around margin-top-md bg-gradual-blue victory-box">
          <view class="victory-content flex-center">
            <view>
              <view class="flex-center victory-content-title">
                <view class="cu-icon-star victory-content-icon" />
                <view>+50</view>
              </view>
              <view class="victory-content-text">找到不同</view>
            </view>
          </view>
          <view class="victory-content flex-center margin-left-md">
            <view>
              <view class="flex-center victory-content-title">
                <view class="cu-icon-time victory-content-icon" />
                <view>+50</view>
              </view>
              <view class="victory-content-text">关卡奖励</view>
            </view>
          </view>
        </view>
        <view class="bg-gradual-green flex-center victory-btn margin-top-md wxy-btn"
              @click="emit('change')">
          继续下一关
        </view>
      </view>
    </view>
  </view>
  <!-- 失败 -->
  <view class="wxy-modal" :class="{'show':show === 2}">
    <view class="wxy-dialog" @click.stop>
      <view class="victory padding-xl bg-white">
        <view class="victory-title">挑战失败</view>
        <view>很遗憾,挑战时间已结束</view>
        <view class="bg-gradual-red flex-center victory-btn margin-top-md wxy-btn"
              @click="emit('change')">
          重新挑战
        </view>
      </view>
    </view>
  </view>
</template>

<script setup lang='ts'>
const emit = defineEmits(['change'])

defineProps({
  show: {
    type: Number,
    default: 0,
  },
})
</script>

<style lang='scss' scoped>
@keyframes lift{
  from{
    transform: translateY(0);
  }

  to{
    transform: translateY(-60rpx);
  }
}

.victory{
  &-img{
    width: 150rpx;
    height: 150rpx;
    margin-top: 40rpx;
    animation: lift 1s cubic-bezier(0.1, 0.6, 0.2, 1) infinite alternate;
  }

  &-title{
    margin-bottom: 10rpx;
    color: #0081ff;
    font-weight: 700;
    font-size: 52rpx;
  }

  &-box{
    padding: 20rpx;
    overflow: hidden;
    border-radius: 20rpx;
  }

  &-content{
    flex: 1;
    height: 120rpx;
    border-radius: 20rpx;

    .cu-icon-star{
      color: var(--main);
    }

    &-icon{
      margin-right: 6rpx;
      font-size: 36rpx;
    }

    &-title{
      font-weight: 700;
      font-size: 36rpx;
    }

    &-text{
      margin-top: 6rpx;
      color: rgb(255 255 255 / 90%);
      font-size: 24rpx;
    }
  }

  &-btn{
    height: 100rpx;
    font-weight: 700;
    font-size: 32rpx;
    border-radius: 20rpx;
  }
}
</style>

data.ts

复制代码
import { ref } from 'vue'

export function useData() {
  /** 刷新 */
  const refresh = ref(false)

  /** 关卡 */
  const level = ref(0)
  const levelTitle = ref('')

  /** 图片 */
  const list = ref<string[]>([])

  /** 答案 */
  const differences = ref<{ x: number; y: number }[]>([])

  /** 正确列表 */
  const correct = ref<{x: number, y: number}[]>([])

  /** 提示 */
  const tipsShow = ref(false)
  const tipsNum = ref(3)
  /** 胜利 */
  const victoryShow = ref(0)
  /** 倒计时 */
  const countdown = ref(120)

  const data = [
    {
      title: '第一关 - 学生',
      list: [
        'https://c-ssl.dtstatic.com/uploads/blog/202507/26/aLSLb1vXc0V0Evm.thumb.1000_0.png',
        'https://c-ssl.dtstatic.com/uploads/blog/202507/26/aLSLb1vXc0V0Evm.thumb.1000_0.png',
      ],
      differences: [
        { x: 60, y: 100 },
        { x: 30, y: 30 },
        { x: 120, y: 200 },
      ],
    },
    {
      title: '第二关 - 落叶',
      list: [
        'https://c-ssl.dtstatic.com/uploads/item/201809/08/20180908231317_tmkfh.thumb.1000_0.jpg',
        'https://c-ssl.dtstatic.com/uploads/item/201809/08/20180908231317_tmkfh.thumb.1000_0.jpg',
      ],
      differences: [
        { x: 60, y: 100 },
        { x: 30, y: 30 },
        { x: 120, y: 200 },
        { x: 300, y: 160 },
      ],
    },
    {
      title: '第三关 - 郊游',
      list: [
        'https://c-ssl.dtstatic.com/uploads/blog/202303/03/20230303161732_038dc.thumb.1000_0.jpg',
        'https://c-ssl.dtstatic.com/uploads/blog/202303/03/20230303161732_038dc.thumb.1000_0.jpg',
      ],
      differences: [
        { x: 60, y: 100 },
        { x: 30, y: 30 },
        { x: 120, y: 200 },
        { x: 300, y: 160 },
        { x: 260, y: 60 },
      ],
    },
  ]

  return {
    refresh,
    level,
    levelTitle,
    list,
    differences,
    correct,
    tipsShow,
    tipsNum,
    victoryShow,
    countdown,
    data,
  }
}

遇到问题可以看我主页加我,很少看博客,对你有帮助别忘记点赞收藏。

相关推荐
CreasyChan2 小时前
Unity UniRx Observable 类详解及使用
游戏·unity·c#·游戏引擎
风月歌2 小时前
小程序项目之超市售货管理平台小程序源代码(源码+文档)
java·微信小程序·小程序·毕业设计·源码
风月歌3 小时前
基于小程序的超市购物系统设计与实现源码(java+小程序+mysql+vue+文档)
java·mysql·微信小程序·小程序·毕业设计·源码
智算菩萨3 小时前
迷宫生成算法:从生成树到均匀随机,再到工程化 Python 实现
python·算法·游戏
小离a_a3 小时前
uniapp微信小程序实现拍照加水印,水印上添加当前时间,当前地点等信息,地点逆解析使用的是高德地图
微信小程序·小程序·uni-app
天呐草莓3 小时前
企业微信运维手册
java·运维·网络·python·微信小程序·企业微信·微信开放平台
鲁Q同志3 小时前
微信小程序树形选择组件
微信小程序·小程序
我叫逢14 小时前
一键去水印实战已上线!心得~
微信小程序·php·去水印
腾讯WeTest14 小时前
范式转移:LLM如何重塑游戏自动化测试的底层逻辑
功能测试·游戏·ai·腾讯wetest