|
|
@@ -0,0 +1,187 @@
|
|
|
+<!-- 文本点击验证组件 -->
|
|
|
+<template>
|
|
|
+ <div class="click-verify">
|
|
|
+ <div class="click-verify__title">
|
|
|
+ <div>
|
|
|
+ <slot name="tip" :mode="state.compareMode">
|
|
|
+ <span>请点击 <b>{{ state.compareMode ? '最大' : '最小' }}</b> 的数字</span>
|
|
|
+ </slot>
|
|
|
+ </div>
|
|
|
+ <div @click="onRefresh">
|
|
|
+ <slot name="refresh">
|
|
|
+ <span>刷新</span>
|
|
|
+ </slot>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div ref="imageRef" class="click-verify__image">
|
|
|
+ <template v-for="(item, index) in state.textList" :key="index">
|
|
|
+ <span :style="item.styles" @click="onClick(item.value)">{{ item.value }}</span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { onMounted, ref, reactive, CSSProperties } from 'vue'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ count: {
|
|
|
+ type: Number,
|
|
|
+ default: 4
|
|
|
+ },
|
|
|
+ // 随机旋转
|
|
|
+ transform: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['success', 'error'])
|
|
|
+
|
|
|
+const imageRef = ref<HTMLDivElement>()
|
|
|
+
|
|
|
+const state = reactive<{
|
|
|
+ textList: {
|
|
|
+ styles: CSSProperties;
|
|
|
+ value: number;
|
|
|
+ }[]
|
|
|
+ textSize: number;
|
|
|
+ positionX: number[];
|
|
|
+ positionY: number[];
|
|
|
+ compareMode: number; // 取大或取小
|
|
|
+}>({
|
|
|
+ textList: [],
|
|
|
+ textSize: 40,
|
|
|
+ positionX: [],
|
|
|
+ positionY: [],
|
|
|
+ compareMode: 0
|
|
|
+})
|
|
|
+
|
|
|
+// 生成随机数组
|
|
|
+const randomNumbers = (min = 1, max = 100) => {
|
|
|
+ // 创建包含所有可能数字的数组
|
|
|
+ const allNumbers = Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
|
|
+
|
|
|
+ // Fisher-Yates洗牌算法
|
|
|
+ for (let i = allNumbers.length - 1; i > 0; i--) {
|
|
|
+ const j = Math.floor(Math.random() * (i + 1));
|
|
|
+ [allNumbers[i], allNumbers[j]] = [allNumbers[j], allNumbers[i]];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 返回前count个元素
|
|
|
+ return allNumbers.slice(0, props.count);
|
|
|
+}
|
|
|
+
|
|
|
+const onRefresh = () => {
|
|
|
+ const el = imageRef.value
|
|
|
+
|
|
|
+ if (el) {
|
|
|
+ const numbers = randomNumbers(4)
|
|
|
+ state.compareMode = Math.floor(Math.random() * 2)
|
|
|
+ state.positionX = []
|
|
|
+ state.positionY = []
|
|
|
+
|
|
|
+ state.textList = numbers.map((value) => {
|
|
|
+ const { top, left } = getItemRandomTopLeft(el.clientWidth, el.clientHeight)
|
|
|
+ return {
|
|
|
+ styles: {
|
|
|
+ top: top + 'px',
|
|
|
+ left: left + 'px',
|
|
|
+ color: getRandomColor(),
|
|
|
+ transform: props.transform ? `rotate(${Math.random() * 360}deg)` : 'none',
|
|
|
+ width: state.textSize + 'px',
|
|
|
+ height: state.textSize + 'px',
|
|
|
+ fontSize: `clamp(12px, calc(${state.textSize}px * 0.5), ${state.textSize}px)`
|
|
|
+ },
|
|
|
+ value
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onClick = (value: number) => {
|
|
|
+ const values = state.textList.map((e) => e.value)
|
|
|
+ const checkedValue = state.compareMode ? Math.max(...values) : Math.min(...values)
|
|
|
+
|
|
|
+ if (checkedValue === value) {
|
|
|
+ emit('success')
|
|
|
+ } else {
|
|
|
+ onRefresh()
|
|
|
+ emit('error')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成随机的RGB颜色值
|
|
|
+const getRandomColor = (minBrightness = 128) => {
|
|
|
+ return '#' + Array.from({ length: 3 }, () => {
|
|
|
+ const value = Math.floor(Math.random() * (256 - minBrightness)) + minBrightness;
|
|
|
+ return value.toString(16).padStart(2, '0');
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+// 判断生成的边界值
|
|
|
+const getItemRandomTopLeft = (width: number, height: number) => {
|
|
|
+ const padding = 20; // 元素之间的最小间距
|
|
|
+
|
|
|
+ // 生成所有可能的位置
|
|
|
+ const positions = [];
|
|
|
+ const cols = Math.floor(width / (state.textSize + padding));
|
|
|
+ const rows = Math.floor(height / (state.textSize + padding));
|
|
|
+
|
|
|
+ for (let i = 0; i < rows; i++) {
|
|
|
+ for (let j = 0; j < cols; j++) {
|
|
|
+ positions.push({
|
|
|
+ x: padding + j * (state.textSize + padding),
|
|
|
+ y: padding + i * (state.textSize + padding)
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 随机打乱位置数组
|
|
|
+ for (let i = positions.length - 1; i > 0; i--) {
|
|
|
+ const j = Math.floor(Math.random() * (i + 1));
|
|
|
+ [positions[i], positions[j]] = [positions[j], positions[i]];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 选择一个不重叠的位置
|
|
|
+ for (const pos of positions) {
|
|
|
+ let collision = false;
|
|
|
+
|
|
|
+ // 检查是否与已放置的元素重叠
|
|
|
+ for (let k = 0; k < state.positionX.length; k++) {
|
|
|
+ const existingX = state.positionX[k];
|
|
|
+ const existingY = state.positionY[k];
|
|
|
+
|
|
|
+ // 检查两个矩形是否重叠
|
|
|
+ if (!(pos.x + state.textSize < existingX ||
|
|
|
+ pos.x > existingX + state.textSize ||
|
|
|
+ pos.y + state.textSize < existingY ||
|
|
|
+ pos.y > existingY + state.textSize)) {
|
|
|
+ collision = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!collision) {
|
|
|
+ state.positionX.push(pos.x);
|
|
|
+ state.positionY.push(pos.y);
|
|
|
+ return {
|
|
|
+ left: pos.x,
|
|
|
+ top: pos.y
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果所有位置都重叠(不太可能),返回一个安全位置
|
|
|
+ return {
|
|
|
+ left: padding,
|
|
|
+ top: padding
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => onRefresh())
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less">
|
|
|
+@import './index.less';
|
|
|
+</style>
|