php+vue3实现点选验证码

buildadmin 中的点选验证码实现
验证码类

<?php

namespace ba;

use Throwable;
use think\facade\Db;
use think\facade\Lang;
use think\facade\Config;

/**
 * 点选文字验证码类
 */
class ClickCaptcha
{
    /**
     * 验证码过期时间(s)
     * @var int
     */
    private int $expire = 600;

    /**
     * 可以使用的背景图片路径
     * @var array
     */
    private array $bgPaths = [
        'static/images/captcha/click/bgs/1.png',
        'static/images/captcha/click/bgs/2.png',
        'static/images/captcha/click/bgs/3.png',
    ];

    /**
     * 可以使用的字体文件路径
     * @var array
     */
    private array $fontPaths = [
        'static/fonts/zhttfs/SourceHanSansCN-Normal.ttf',
    ];

    /**
     * 验证点 Icon 映射表
     * @var array
     */
    private array $iconDict = [
        'aeroplane' => '飞机',
        'apple'     => '苹果',
        'banana'    => '香蕉',
        'bell'      => '铃铛',
        'bicycle'   => '自行车',
        'bird'      => '小鸟',
        'bomb'      => '炸弹',
        'butterfly' => '蝴蝶',
        'candy'     => '糖果',
        'crab'      => '螃蟹',
        'cup'       => '杯子',
        'dolphin'   => '海豚',
        'fire'      => '火',
        'guitar'    => '吉他',
        'hexagon'   => '六角形',
        'pear'      => '梨',
        'rocket'    => '火箭',
        'sailboat'  => '帆船',
        'snowflake' => '雪花',
        'wolf head' => '狼头',
    ];

    /**
     * 配置
     * @var array
     */
    private array $config = [
        // 透明度
        'alpha' => 36,
        // 中文字符集
        'zhSet' => '们以我到他会作时要动国产的是工就年阶义发成部民可出能方进在和有大这主中为来分生对于学级地用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所起政好十战无农使前等反体合斗路图把结第里正新开论之物从当两些还天资事队点育重其思与间内去因件利相由压员气业代全组数果期导平各基或月然如应形想制心样都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极已根共直团统式转别造切九你取西持总料连任志观调么山程百报更见必真保热委手改管处己将修支识象先老光专什六型具示复安带每东增则完风回南劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单坚据速防史拉世设达尔场织历花求传断况采精金界品判参层止边清至万确究书术状须离再目海权且青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿胜细影济白格效置推空配叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非亚磨族段算适讲按值美态易彪服早班麦削信排台声该击素张密害侯何树肥继右属市严径螺检左页抗苏显苦英快称坏移巴材省黑武培著河帝仅针怎植京助升王眼她抓苗副杂普谈围食源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功友限项余倒卷创律雨让骨远帮初皮播优占圈伟季训控激找叫云互跟粮粒母练塞钢顶策双留误础阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺版烈零室轻倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送侧润盖挥距触星松送获兴独官混纪依未突架宽冬章偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞哪旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶念兰映沟乙吗儒汽磷艰晶埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀摆贡呈劲财仪沉炼麻祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜脂庄擦险赞钟摇典柄辩竹谷乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼峰零柴簧午跳居尚秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑冰柬嘴啥饭塑寄赵喊垫丹渡耳虎笔稀昆浪萨茶滴浅拥覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷忽闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳塘燥泡袋朗喂铝软渠颗惯贸综墙趋彼届墨碍启逆卸航衣孙龄岭休借',
    ];

    /**
     * 构造方法
     * @param array $config 点击验证码配置
     * @throws Throwable
     */
    public function __construct(array $config = [])
    {
        $clickConfig  = Config::get('buildadmin.click_captcha');
		//这里会得配置文件中的数据合并
		//$clickConfig 中的配置是这样的
		//    'click_captcha'         => [
        // 模式:text=文字,icon=图标(若只有icon则适用于国际化站点)
        //'mode'           => ['text', 'icon'],
        // 长度
        //'length'         => 2,
        // 混淆点长度
        //'confuse_length' => 2,
    ],
        $this->config = array_merge($clickConfig, $this->config, $config);
        // 清理过期的验证码
        Db::name('captcha')->where('expire_time', '<', time())->delete();
    }

    /**
     * 创建图形验证码
     * @param string $id 验证码ID,开发者自定义
     * @return array 返回验证码图片的base64编码和验证码文字信息
     */
    public function creat(string $id): array
    {
        $imagePath  = Filesystem::fsFit(public_path() . $this->bgPaths[mt_rand(0, count($this->bgPaths) - 1)]);  //随机一个背景图片
        $fontPath   = Filesystem::fsFit(public_path() . $this->fontPaths[mt_rand(0, count($this->fontPaths) - 1)]);  //随机一个字体

        $randPoints = $this->randPoints($this->config['length'] + $this->config['confuse_length']);  //生成验证码的长度, 加上混肖点的长度相加
        $lang = Lang::getLangSet();

        foreach ($randPoints as $v) {
            $tmp['size'] = rand(15, 30);
            if (isset($this->iconDict[$v])) {
                // 图标
                $tmp['icon']   = true;
                $tmp['name']   = $v;
                $tmp['text']   = $lang == 'zh-cn' ? "<{$this->iconDict[$v]}>" : "<$v>";
                $iconInfo      = getimagesize(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $v . '.png'));
                $tmp['width']  = $iconInfo[0];   //$size = getimagesize($filename);  $size[0]: 图像的宽度 $size[1]: 图像的高度
                $tmp['height'] = $iconInfo[1];
            } else {
                // 字符串文本框宽度和长度
                $fontArea      = imagettfbbox($tmp['size'], 0, $fontPath, $v);
                $textWidth     = $fontArea[2] - $fontArea[0];  //得到文字的宽度
                $textHeight    = $fontArea[1] - $fontArea[7];   //得到文字的高度
                $tmp['icon']   = false;  //说明这个不是图片
                $tmp['text']   = $v;
                $tmp['width']  = $textWidth;        //文字的宽度
                $tmp['height'] = $textHeight;      //文字的高度
            }
            $textArr['text'][] = $tmp;
        }
        // 图片宽高和类型
        $imageInfo         = getimagesize($imagePath);
        $textArr['width']  = $imageInfo[0];  //$textArr 的宽度 是背景图宽度
        $textArr['height'] = $imageInfo[1];  //$testArr 的高度 是背景图高度
        // 随机生成验证点位置


        foreach ($textArr['text'] as &$v) {
            list($x, $y) = $this->randPosition($textArr['text'], $textArr['width'], $textArr['height'], $v['width'], $v['height'], $v['icon']);
            $v['x'] = $x;
            $v['y'] = $y;
            $text[] = $v['text'];  //这里的把生成的标记也按顺序 记录了下来
        }

        unset($v);
        ;
        // 创建图片的实例
        $image = imagecreatefromstring(file_get_contents($imagePath));
        foreach ($textArr['text'] as $v) {
            if ($v['icon']) {
                $this->iconCover($image, $v);
            } else {
                //字体颜色
                $color = imagecolorallocatealpha($image, 239, 239, 234, 127 - intval($this->config['alpha'] * (127 / 100)));
                // 绘画文字
                imagettftext($image, $v['size'], 0, $v['x'], $v['y'], $color, $fontPath, $v['text']);
            }
        }
        $nowTime         = time();
        $textArr['text'] = array_splice($textArr['text'], 0, $this->config['length']);   //取了两个
        $text            = array_splice($text, 0, $this->config['length']);   //前两个的text ,用来返回给前端用的

        Db::name('captcha')
            ->replace()
            ->insert([
                'key'         => md5($id),
                'code'        => md5(implode(',', $text)),
                'captcha'     => json_encode($textArr, JSON_UNESCAPED_UNICODE),
                'create_time' => $nowTime,
                'expire_time' => $nowTime + $this->expire
            ]);

        // 输出图片
        while (ob_get_level()) {
            ob_end_clean();
        }
        if (!ob_get_level()) ob_start();
        switch ($imageInfo[2]) {
            case 1:// GIF
                imagegif($image);
                $content = ob_get_clean();
                break;
            case 2:// JPG
                imagejpeg($image);
                $content = ob_get_clean();
                break;
            case 3:// PNG
                imagepng($image);
                $content = ob_get_clean();
                break;
            default:
                $content = '';
                break;
        }
        imagedestroy($image);
        return [
            'id'     => $id,
            'text'   => $text,
            'base64' => 'data:' . $imageInfo['mime'] . ';base64,' . base64_encode($content),
            'width'  => $textArr['width'],
            'height' => $textArr['height'],
        ];
    }

    /**
     * 检查验证码
     * @param string $id    开发者自定义的验证码ID
     * @param string $info  验证信息
     * @param bool   $unset 验证成功是否删除验证码
     * @return bool
     * @throws Throwable
     */
    public function check(string $id, string $info, bool $unset = true): bool
    {
        $key     = md5($id);
        $captcha = Db::name('captcha')->where('key', $key)->find();
        if ($captcha) {
            // 验证码过期
            if (time() > $captcha['expire_time']) {
                Db::name('captcha')->where('key', $key)->delete();
                return false;
            }
            $textArr = json_decode($captcha['captcha'], true);
            list($xy, $w, $h) = explode(';', $info);
            $xyArr = explode('-', $xy);
            //xyArr[0] 249,112    xyArr[1]47,68
            $xPro  = $w / $textArr['width'];// 宽度比例
            $yPro  = $h / $textArr['height'];// 高度比例
            foreach ($xyArr as $k => $v) {
                $xy = explode(',', $v);
                $x  = $xy[0];  //249
                $y  = $xy[1];   //112
                if ($x / $xPro < $textArr['text'][$k]['x'] || $x / $xPro > $textArr['text'][$k]['x'] + $textArr['text'][$k]['width']) {
                    return false;
                }
                $phStart = $textArr['text'][$k]['icon'] ? $textArr['text'][$k]['y'] : $textArr['text'][$k]['y'] - $textArr['text'][$k]['height'];
                $phEnd   = $textArr['text'][$k]['icon'] ? $textArr['text'][$k]['y'] + $textArr['text'][$k]['height'] : $textArr['text'][$k]['y'];
                if ($y / $yPro < $phStart || $y / $yPro > $phEnd) {
                    return false;
                }
            }
            if ($unset) Db::name('captcha')->where('key', $key)->delete();
            return true;
        } else {
            return false;
        }
    }

    /**
     * 绘制Icon
     */
    protected function iconCover($bgImg, $iconImgData): void
    {
        $iconImage      = imagecreatefrompng(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $iconImgData['name'] . '.png'));
        $trueColorImage = imagecreatetruecolor($iconImgData['width'], $iconImgData['height']);
        imagecopy($trueColorImage, $bgImg, 0, 0, $iconImgData['x'], $iconImgData['y'], $iconImgData['width'], $iconImgData['height']);
        imagecopy($trueColorImage, $iconImage, 0, 0, 0, 0, $iconImgData['width'], $iconImgData['height']);
        imagecopymerge($bgImg, $trueColorImage, $iconImgData['x'], $iconImgData['y'], 0, 0, $iconImgData['width'], $iconImgData['height'], $this->config['alpha']);
        imagedestroy($iconImage);
        imagedestroy($trueColorImage);
    }

    /**
     * 随机生成验证点元素
     * @param int $length
     * @return array
     */
    public function randPoints(int $length = 4): array
    {
        $arr = [];

        // 文字
        if (in_array('text', $this->config['mode'])) {
            for ($i = 0; $i < $length; $i++) {
                $arr[] = mb_substr($this->config['zhSet'], mt_rand(0, mb_strlen($this->config['zhSet'], 'utf-8') - 1), 1, 'utf-8');
            }
        }
        //这里生成了 4 个文字


        // 图标
        if (in_array('icon', $this->config['mode'])) {
            $icon = array_keys($this->iconDict); //得到所有的图片的 key
            shuffle($icon);  //打乱key的顺序
            $icon = array_slice($icon, 0, $length);  //截取4个图片的key
            $arr  = array_merge($arr, $icon);  //把生成的 文字和图片的 数组合并
        }

        shuffle($arr); //打乱顺序
        return array_slice($arr, 0, $length);  //取出前4个
    }

    /**
     * 随机生成位置布局
     * @param array $textArr 点位数据
     * @param int   $imgW    图片宽度
     * @param int   $imgH    图片高度
     * @param int   $fontW   文字宽度
     * @param int   $fontH   文字高度
     * @param bool  $isIcon  是否是图标
     * @return array
     */
    private function randPosition(array $textArr, int $imgW, int $imgH, int $fontW, int $fontH, bool $isIcon): array
    {
        $x = rand(0, $imgW - $fontW);
        $y = rand($fontH, $imgH - $fontH);
        // 碰撞验证
        if (!$this->checkPosition($textArr, $x, $y, $fontW, $fontH, $isIcon)) {
            $position = $this->randPosition($textArr, $imgW, $imgH, $fontW, $fontH, $isIcon);
        } else {
            $position = [$x, $y];
        }
        return $position;
    }

    /**
     * 碰撞验证
     * @param array $textArr 验证点数据
     * @param int   $x       x轴位置
     * @param int   $y       y轴位置
     * @param int   $w       验证点宽度
     * @param int   $h       验证点高度
     * @param bool  $isIcon  是否是图标
     * @return bool
     */
    public function checkPosition(array $textArr, int $x, int $y, int $w, int $h, bool $isIcon): bool
    {
        $flag = true;
        foreach ($textArr as $v) {
            if (isset($v['x']) && isset($v['y'])) {
                $flagX     = false;
                $flagY     = false;
                $historyPw = $v['x'] + $v['width'];
                if (($x + $w) < $v['x'] || $x > $historyPw) {
                    $flagX = true;
                }

                $currentPhStart = $isIcon ? $y : $y - $h;
                $currentPhEnd   = $isIcon ? $y + $v['height'] : $y;
                $historyPhStart = $v['icon'] ? $v['y'] : ($v['y'] - $v['height']);
                $historyPhEnd   = $v['icon'] ? ($v['y'] + $v['height']) : $v['y'];
                if ($currentPhEnd < $historyPhStart || $currentPhStart > $historyPhEnd) {
                    $flagY = true;
                }
                if (!$flagX && !$flagY) {
                    $flag = false;
                }
            }
        }
        return $flag;
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
这里的知识点,和验证的时候,图片和文字的 x 坐标和 y 坐标的对比不一样是有关系的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


前端代码
前端通过代码, 请求 后台的 验证码的 creat ,得到图片,并显示到前端页面

<template>
    <div :id="uuid">
        <div class="ba-click-captcha" :class="props.class">
            <div v-if="state.loading" class="loading">{{ i18n.global.t('utils.Loading') }}</div>
            <div v-else class="captcha-img-box">
                <img
                    class="captcha-img"
                    @click.prevent="onRecord($event)"
                    :src="state.captcha.base64"
                    :alt="i18n.global.t('validate.Captcha loading failed, please click refresh button')"
                />
                <span
                    v-for="(item, index) in state.xy"
                    :key="index"
                    class="step"
                    @click="onCancelRecord(index)"
                    :style="`left:${parseFloat(item.split(',')[0]) - 13}px;top:${parseFloat(item.split(',')[1]) - 13}px`"
                >
                    {{ index + 1 }}
                </span>
            </div>
            <div class="captcha-prompt" v-if="state.tip">
                {{ state.tip }}
            </div>
            <div v-else class="captcha-prompt">
                {{ i18n.global.t('validate.Please click') }}
                <span v-for="(text, index) in state.captcha.text" :key="index" :class="state.xy.length > index ? 'clicaptcha-clicked' : ''">
                    {{ text }}
                </span>
            </div>
            <div class="captcha-refresh-box">
                <div class="captcha-refresh-line captcha-refresh-line-l"></div>
                <i class="fa fa-refresh captcha-refresh-btn" :title="i18n.global.t('Refresh')" @click="load"></i>
                <div class="captcha-refresh-line captcha-refresh-line-r"></div>
            </div>
        </div>
        <div class="ba-layout-shade" @click="onClose"></div>
    </div>
</template>

<script setup lang="ts">
import { reactive, computed } from 'vue'
import { getCaptchaData, checkClickCaptcha } from '/@/api/common'
import { i18n } from '/@/lang'

interface Props {
    uuid: string
    callback?: (captchaInfo: string) => void
    class?: string
    unset?: boolean
    error?: string
    success?: string
}

const props = withDefaults(defineProps<Props>(), {
    uuid: '',
    callback: () => {},
    class: '',
    unset: false,
    error: i18n.global.t('validate.The correct area is not clicked, please try again!'),
    success: i18n.global.t('validate.Verification is successful!'),
})

const state: {
    loading: boolean
    xy: string[]
    tip: string
    captcha: {
        id: string
        text: string
        base64: string
        width: number
        height: number
    }
} = reactive({
    loading: true,
    xy: [],
    tip: '',
    captcha: {
        id: '',
        text: '',
        base64: '',
        width: 350,
        height: 200,
    },
})

const load = () => {
    state.loading = true
    getCaptchaData(props.uuid).then((res) => {
        state.xy = []
        state.tip = ''
        state.loading = false
        state.captcha = res.data
    })
}

const onRecord = (event: MouseEvent) => {
    if (state.xy.length < state.captcha.text.length) {
        state.xy.push(event.offsetX + ',' + event.offsetY)
        if (state.xy.length == state.captcha.text.length) {
            const captchaInfo = [state.xy.join('-'), (event.target as HTMLImageElement).width, (event.target as HTMLImageElement).height].join(';')
            checkClickCaptcha(props.uuid, captchaInfo, props.unset)
                .then(() => {
                    state.tip = props.success
                    setTimeout(() => {
                        props.callback?.(captchaInfo)
                        onClose()
                    }, 1500)
                })
                .catch(() => {
                    state.tip = props.error
                    setTimeout(() => {
                        load()
                    }, 1500)
                })
        }
    }
}

const onCancelRecord = (index: number) => {
    state.xy.splice(index, 1)
}

const onClose = () => {
    document.getElementById(props.uuid)?.remove()
}

const captchaBoxTop = computed(() => (state.captcha.height + 200) / 2 + 'px')
const captchaBoxLeft = computed(() => (state.captcha.width + 24) / 2 + 'px')

load()
</script>

<style scoped lang="scss">
.ba-click-captcha {
    padding: 12px;
    border: 1px solid var(--el-border-color-extra-light);
    background-color: var(--el-color-white);
    position: fixed;
    z-index: 9999991;
    left: calc(50% - v-bind('captchaBoxLeft'));
    top: calc(50% - v-bind('captchaBoxTop'));
    border-radius: 10px;
    box-shadow: 0 0 0 1px hsla(0, 0%, 100%, 0.3) inset, 0 0.5em 1em rgba(0, 0, 0, 0.6);
    .loading {
        color: var(--el-color-info);
        width: 350px;
        text-align: center;
        line-height: 200px;
    }
    .captcha-img-box {
        position: relative;
        .captcha-img {
            width: v-bind('state.captcha.width') px;
            height: v-bind('state.captcha.height') px;
            border: none;
            cursor: pointer;
        }
        .step {
            box-sizing: border-box;
            position: absolute;
            width: 20px;
            height: 20px;
            line-height: 20px;
            font-size: var(--el-font-size-small);
            font-weight: bold;
            text-align: center;
            color: var(--el-color-white);
            border: 1px solid var(--el-border-color-extra-light);
            background-color: var(--el-color-primary);
            border-radius: 30px;
            box-shadow: 0 0 10px var(--el-color-white);
            user-select: none;
            cursor: pointer;
        }
    }
    .captcha-prompt {
        height: 40px;
        line-height: 40px;
        font-size: var(--el-font-size-base);
        text-align: center;
        color: var(--el-color-info);
        span {
            margin-left: 10px;
            font-size: var(--el-font-size-medium);
            font-weight: bold;
            color: var(--el-color-error);
            &.clicaptcha-clicked {
                color: var(--el-color-primary);
            }
        }
    }
    .captcha-refresh-box {
        position: relative;
        margin-top: 10px;
        .captcha-refresh-line {
            position: absolute;
            top: 16px;
            width: 140px;
            height: 1px;
            background-color: #ccc;
        }
        .captcha-refresh-line-l {
            left: 5px;
        }
        .captcha-refresh-line-r {
            right: 5px;
        }
        .captcha-refresh-btn {
            cursor: pointer;
            display: block;
            margin: 0 auto;
            width: 32px;
            height: 32px;
            font-size: 32px;
            color: var(--el-color-info);
        }
    }
}
</style>

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当点击次数达到两次的时候,就提交到后台去验证
前端提交的数据格式是这样的
在这里插入图片描述
id是前后端对应的, info 中 以 “-” 分隔了 两次点击的 坐标点 , 350是图片的完度,200是图片的高度

接下来后端进行验证, 我们来看看后端的验证过程

在这里插入图片描述
在这里插入图片描述

自已写一个试试, 后端的接口还是用的 buildadmin 的接口,前端我自己写了一下简易代码做了一下实验,如果开发的时候 可以使用上面的 代码, 注意上面的代码是 ts 的,稍稍改一下代码就可以了
以下是我用 vue3 js写的简易代码, 也是可以实现验证码的 , 仅供参考

<template>
    <div class="captcha-wrapper">
        <div class="captcha">
            <img v-if="data.captchaInfo.base64" width="300" height="200"  @click.prevent="clickcaptcha($event)" class="img-captcha" :src="data.captchaInfo.base64" />
            <span v-for="(item,index) in data.captchaInfo.clickXY" :style="{left:item.x+'px',top:item.y+'px'}">{{index+1}}</span>
        </div>
        <div>请点击字符或图片:{{data.captchaInfo.text}}</div>
        <button @click="getphoto"> 刷新验证码 </button>
    </div>

</template>


<script setup>
import {onMounted,ref,reactive} from "vue"
import { checkClickCaptcha, getCaptchaData } from '../../api/common'


let uuid = ref("");
let data = reactive({
    captchaInfo:{
        base64:"",
        text:"",
        width:"",   //后端返回的图片的宽高, 一般在显示的时候就按这个大小显示, 本例中没有使用它们,而是自定义了一个 宽高,验证时,要把本地自定义的宽高传给后端才可以
        height:"",
        id:uuid,
        number:0,       //当前图片被点击的次数
        clickposition:"",
        clickXY:[]
    }
})

    //生命周期
    onMounted(()=>{
        console.log(123);
        uuid = Math.floor(Math.random()*(10000-1+1))+1;
        getphoto();  //生命周期开始时调用后台接口,得到 验证码图片
    });

    //图片的点击事件
    let clickcaptcha = (e)=>{
        let xy = e.offsetX+","+e.offsetY;   //得到点击的位置,因为是两个验证码,所以要点击两次
        if(data.captchaInfo.number == 0){
            data.captchaInfo.clickposition = xy;   //如果是第一次点击 记录一下, 点击位置
            data.captchaInfo.clickXY = [{x:e.offsetX,y:e.offsetY}]
        }else {
            data.captchaInfo.clickposition = data.captchaInfo.clickposition + "-" + xy;  //如果是第二次点击 ,把两次点击的位置都记录下来
            data.captchaInfo.clickXY.push({x:e.offsetX,y:e.offsetY})
        }
        data.captchaInfo.number++;
        if(data.captchaInfo.number == 2){  //点击了两次
            checkClickCaptcha(data.captchaInfo.id, data.captchaInfo.clickposition+";"+'300;200', true)
                .then((res) => {
                    console.log(res);
                    if(res.code == 1){
                        //这里验证成功的代码,  验证成功之后, 把captchaInfo 的数据清空
                        //然后提交表单中的数据
                        alert("验证成功")
                    }else if(res.code == 0){
                        alert("验证失败")
                    }
                })
                .catch(() => {
                    alert("验证失败")
                })
        }
    }

    let getphoto = ()=>{
        getCaptchaData(uuid).then(res=>{
            data.captchaInfo = Object.assign(data.captchaInfo,res.data,{ number:0,       //当前图片被点击的次数
                clickposition:"",
                clickXY:[]});
        })
    }


</script>

<style scoped lang="scss">

    .captcha-wrapper{
        width:300px;  //这里要和自定义的图片一样宽
        .captcha{
            position: relative;
            .img-captcha{
            }
            span{
                position:absolute;display:block;width:20px;height:20px;background:#f60;text-align: center;line-height: 20px;border-radius: 10px;color:#fff;
            }
        }
    }
</style>


在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/147097.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【洛谷算法题】P5711-闰年判断【入门2分支结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5711-闰年判断【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格式&a…

【LeetCode刷题笔记】二叉树(二)

257. 二叉树的所有路径 解题思路: 1. DFS 前序遍历 ,每次递归将 当前节点的拼接结果 传递到 下一层 中,如果当前节点是 叶子节点 ,就将 当前拼接结果 收集答案并返回。 注意:路径path结果可以使用 String 来拼接,这样可以避免回溯处理。

Scala---方法与函数

一、Scala方法的定义 有参方法&无参方法 def fun (a: Int , b: Int) : Unit {println(ab) } fun(1,1)def fun1 (a: Int , b: Int) ab println(fun1(1,2)) 注意点&#xff1a; 方法定义语法 用def来定义可以定义传入的参数&#xff0c;要指定传入参数的类型方法可以写返…

时间序列基础->数据标签、数据分割器、数据加载器的定义和讲解(零基础入门时间序列)

一、本文介绍 各位小伙伴好&#xff0c;最近在发时间序列的实战案例中总是有一些朋友问我时间序列中的部分对数据的操作是什么含义&#xff0c;我进行了挺多的介绍和讲解但是问的人越来越多&#xff0c;所以今天在这里单独发一篇文章来单独的讲一下时间序列中对数据的处理操作…

PHP使用文件缓存实现html静态化

<?php // 动态生成的内容 $content "<html><body><h1>time:".date("Y-m-d H:i:s")."</h1></body></html>"; // 静态文件保存路径和文件名 $staticFilePath "file.html"; if(file_exists($s…

【汇编】内存的读写与地址空间、寄存器及数据存储

文章目录 前言一、CPU对存储器的读写1.1 cpu对存储器的读写如何进行&#xff1f;1.2 演示 二、内存地址空间三、将各类存储器看作一个逻辑存储器——统一编址内存地址空间的分配方案 三、CPU的组成寄存器是CPU内部的信息存储单元通用寄存器--AX为例“横看成岭侧成峰“ 四、“字…

工具及方法 - 手机扫条码工具: SCANDIT APP

一般扫个链接使用微信扫一扫即可。扫具体条码&#xff0c;可以在微信里搜索小程序&#xff0c;打开也能扫&#xff0c;得到条码内容。 还有其他方式&#xff0c;比如使用淘宝、百度等APP也可以直接扫码条码&#xff0c;还能得到更多的信息。 使用百度的话&#xff0c;不扫条码…

Vue中methods实现原理

目录 前言 回调函数中的this指向问题 vue实例访问methods methods实现原理 前言 vue实例对象为什么可以访问methods中的函数方法&#xff1f;methods的实现原理是什么&#xff1f; 回调函数中的this指向问题 在解答前言中的问题前&#xff0c;需要了解一下回调函数中的th…

振南技术干货集:深入浅出的Bootloader(2)

注解目录 1、烧录方式的更新迭代 1.1 古老的烧录方式 (怀旧一下&#xff0c;单片机高压烧录器。) 1.2 ISP 与ICP 烧录方式 (还记得当年我们玩过的 AT89S51?) 1.3 更方便的 ISP 烧录方式 1.3.1串口 ISP &#xff08;是 STC 单片机成就了我们&#xff0c;还是我们成就了…

C#,数值计算——函数计算,Ratfn的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public class Ratfn { private double[] cofs { get; set; } private int nn { get; set; } private int dd { get; set; } public Ratfn(double[] num, double[] den) { …

编辑器vim和编译器gcc/g++

目录 一、编辑器vim 1、概念 2、基本操作 1、进入vim 2、模式切换 3、命令行模式 4、插入模式 5、底行模式 6、vim 的配置 二、编译器gcc/g 1、概念 2、背景知识 3、gcc/g中的编译链接 1、预处理 2、编译 3、汇编 4、链接 4、函数库 1、静态库 2、动态库 一…

服务器数据恢复—服务器发生故障导致数据丢失如何恢复服务器数据?

服务器常见故障&#xff1a; 硬件故障&#xff1a;磁盘、板卡、电源故障等。 软件故障&#xff1a;操作系统崩溃、程序运行错误等。 入侵破坏&#xff1a;加密、删除服务数据等。 不可控力&#xff1a;浸水、火烧、倒塌等。 误操作&#xff1a;格式化、删除、覆盖等。 如何减少…

详解[ZJCTF 2019]NiZhuanSiWei 1(PHP两种伪协议、PHP反序列化漏洞、PHP强比较)还有那道题有这么经典?

题目环境&#xff1a; <?php $text $_GET["text"]; $file $_GET["file"]; $password $_GET["password"]; if(isset($text)&&(file_get_contents($text,r)"welcome to the zjctf")){echo "<br><h1>&…

WebStorm配置less编译wxss或css

文章目录 前言先下载安装less程序&#xff1a;实参&#xff1a;要刷新的输出路径成功 前言 使用WebStorm写微信小程序&#xff0c;wxss写着很麻烦&#xff0c;就想着用less&#xff0c;接下来是配置less编译 先下载安装less npm install -g lessless会安装在你当前目录下(以D…

虹科示波器 | 汽车免拆检修 | 2014款保时捷卡宴车行驶中发动机偶尔自动熄火

一、故障现象 一辆2014款保时捷卡宴车&#xff0c;搭载4.8L自然吸气发动机&#xff0c;累计行驶里程约为10.3万km。车主反映&#xff0c;行驶中发动机偶尔自动熄火&#xff0c;尤其在减速至停车的过程中故障容易出现。 二、故障诊断 接车后路试&#xff0c;确认故障现象与车主所…

caspp attacker lab

attacker lab phase2 advice phase 1 ctarget 会先调用test , test调用getbuf, getbuf调用Get。 任务目的是通过缓冲区注入攻击&#xff0c;将函数getbuf返回直接重定向到函数touch1。 0x28 是 40 比特&#xff0c; gdb ./ctarget getbuf 下一次执行的指令是401976, rsp对…

Vue快速入门

目录 一、概述 环境准备 前置知识 JavaScript-导入导出 默认导入导出 二、局部使用Vue 1、使用步骤 准备工作 构建用户界面 2、常用指令 v-for v-bind v-if & v-show v-on v-model 3、生命周期 三、Axios 使用案例1 测试 使用案例2 测试 请求方式别名…

ARMday06(总线、串口、RCC章节分析)

总线 总线是完成各个部件之间传输的一种媒介 串行/并行总线 串行总线&#xff0c; 在同一时刻&#xff0c;根据时钟线的变化&#xff0c;只可以收发一位数据 优点&#xff1a;占用引脚资源少 缺点&#xff1a;传输速度比较慢 并行总线&#xff0c; 在同一时刻&#xff…

vscode设置vue3代码格式化

vscode设置vue3代码格式化 vscode设置vue3代码格式化 下载插件设置格式化时选用的插件实际使用 使用Prettier默认配置使用Prettier添加自定义配置使用Volar 完整配置文件参考链接 下载插件 可以使用Volar或Prettier 设置格式化时选用的插件 mac&#xff1a;【shift】【op…

基于单片机微波炉加热箱系统设计

**单片机设计介绍&#xff0c; 基于单片机微波炉加热箱系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的微波炉加热箱系统是一种智能化的厨房电器设备&#xff0c;利用单片机控制技术实现自动加热和定时等功能…