php+vue3实现点选验证码

news2024/11/20 11:27:11

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>


在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

萌宠俱乐部

一、html代码 二、CSS代码 三、效果图 四、继续努力呀&#xff01;&#xff01;&#xff01; 一、html代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"wi…

如何使用线性模型的【分箱】操作处理非线性问题

让线性回归在非线性数据上表现提升的核心方法之一是对数据进行分箱&#xff0c;也就是离散化。与线性回归相比&#xff0c;我们常用的一种回归是决策树的回归。为了对比不同分类器和分箱前后拟合效果的差异&#xff0c;我们设置对照实验。 生成一个非线性数据集前&#xff0c;…

Java计算数据百分比

public class CalculatePCT {public static void main(String[] args) {System.out.println(getPercent(9, 100));System.out.println(getPercent2(3, 7));}/*** 方式一&#xff1a;使用java.text.NumberFormat实现*/public static String getPercent(int x, int y) {double d1…

ACM练习——第二天

今天又是一天课&#xff0c;满课&#xff0c;很累哈&#xff0c;计组真的挺难的&#xff0c;但是多学学还是可以学明白。行吧&#xff0c;继续进入今天的ACM练习&#xff0c;现阶段都是主要练习Java到C的语言过渡。 因为今天的题目多半都是昨天的延伸&#xff0c;我就不提供Jav…

Java主流分布式解决方案多场景设计与实战

Java的主流分布式解决方案的设计和实战涉及到多个场景&#xff0c;包括但不限于以下几点&#xff1a; 分布式缓存&#xff1a;在Java的分布式系统中&#xff0c;缓存是非常重要的一部分。常用的分布式缓存技术包括Redis、EhCache等。这些缓存技术可以用来提高系统的性能和响应…

2023软件测试面试跳槽必备

你眼中的软件测试岗位是怎样的&#xff1f;大部分人可能会给出这样的回答&#xff1a;“测试&#xff1f;简单啊&#xff0c;没什么技术含量&#xff0c;无非就是看需求、看业务手册、看设计文档、然后点点功能是否实现&#xff0c;麻烦点的就是测试下部署安装是否出现兼容性问…

开讲:长江航道工程局举办首届云表无代码培训班

11月9日至10日&#xff0c;公司联合珠海乐图软件有限公司在总部机关举办了首届云表无代码编程开发初级培训班。公司所属单位工程、成本、财务等相关业务部门及项目部管理人员参加培训&#xff0c;公司总工程师张晏方作开班动员讲话。 张晏方指出&#xff0c;公司自主开发的云表…

java生成docx文档, docx文档动态饼图

背景: 最近接了个需求, 要求生成日报, 大概如下图所示: 其中*表示变量, 看到要动态生成doc给我难受坏了,为什么会有这种需求? 然后看到里面还要动态生成饼图, oh, no.........没有办法, 硬着头皮上吧. 于是就搜了下java生成docx的方式, 看到的, 比较靠谱的一种通过freemake…

【每日一题】1334. 阈值距离内邻居最少的城市-2023.11.14

题目&#xff1a; 1334. 阈值距离内邻居最少的城市 有 n 个城市&#xff0c;按从 0 到 n-1 编号。给你一个边数组 edges&#xff0c;其中 edges[i] [fromi, toi, weighti] 代表 fromi 和 toi 两个城市之间的双向加权边&#xff0c;距离阈值是一个整数 distanceThreshold。 …

[Linux] ssh远程访问及控制

一、ssh介绍 1.1 SSH简介 SSH&#xff08;Secure Shell&#xff09;是一种安全通道协议&#xff0c;主要用于实现远程登录、远程复制等功能的字符接口。SSH 协议包括用户在登录时输入的用户密码、双方之间的通信。 加密数据传输&#xff0c;SSH 是一种建立在应用层和传输层上…

<MySQL> 查询数据进阶操作 -- 聚合查询

目录 一、聚合查询概述 二、聚合函数查询 2.1 常用函数 2.2 使用函数演示 2.3 聚合函数参数为*或列名的查询区别 2.4 字符串不能参与数学运算 2.5 具有误导性的结果集 三、分组查询 group by 四、分组后条件表达式查询 五、MySQL 中各个关键字的执行顺序 一、聚合查询…

【2013年数据结构真题】

highlight: a11y-dark 41题 王道解析&#xff1a; 算法的策略是从前向后扫描数组元素&#xff0c;标记出一个可能成为主元素的元素Num 。然后重新计数&#xff0c;确认Num是否是主元素。算法可分为以下两步&#xff1a; 选取候选的主元素&#xff1a;依次扫描所给数组中的每个…

【数据结构 | 链表】leetcode 2. 两数相加

个人主页&#xff1a;兜里游客棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里游客棉花糖 原创 收录于专栏【LeetCode】 原题链接&#xff1a;点击直接跳转到该题目 目录 题目描述解题代码 题目描述 给你两个 非空 的链表&#xff0c;表示两个非…

降低城市内涝风险,万宾科技内涝积水监测仪的作用

频繁的内涝会削弱和损坏城市的关键基础设施&#xff0c;包括道路、桥梁和公用设施。城市内涝风险降低可以减少交通中断事件&#xff0c;也可以保护居民安全并降低路面维修等成本&#xff0c;进一步确保城市基本服务继续发挥作用。对城市可持续发展来讲有效减少内涝的风险是重要…

ESP32网络开发实例-将DS18B20传感器读数发送到InfluxDB

将DS18B20传感器读数发送到InfluxDB 文章目录 将DS18B20传感器读数发送到InfluxDB1、InfluxDB、DS18B20介绍2、软件准备3、硬件准备4、代码实现在本文中,我们将介绍如何将 DS18B20传感器读数发送到 InfluxDB 时间序列数据库。 使用 InfluxDB 数据库的一大特点是可以在确定的时…

python 爬虫之requests 库以及相关函数的详细介绍

get 函数 当你使用 requests.get 函数时&#xff0c;你可以按照以下步骤来发起一个 GET 请求&#xff1a; 导入 requests 模块&#xff1a; 在你的 Python 脚本或程序中&#xff0c;首先导入 requests 模块。 import requests指定目标 URL&#xff1a; 设置你要请求的目标 URL…

4路光栅尺磁栅尺编码器解码转换5MHz高速差分信号转Modbus TCP网络模块 YL97-RJ45

特点&#xff1a; ● 光栅尺磁栅尺解码转换成标准Modbus TCP协议 ● 光栅尺5V差分信号直接输入&#xff0c;4倍频计数 ● 模块可以输出5V的电源给光栅尺供电 ● 高速光栅尺磁栅尺计数&#xff0c;频率可达5MHz ● 支持4个光栅尺同时计数&#xff0c;可识别正反转 ● 可网…

啊?印第安碳纤维限量款?复古与性能的结合吗Indian FTR x 100% R Carbon

印第安作为美国的老牌摩托车厂大家都不陌生了&#xff0c;和哈雷有一点比较大的区别是印第安的车还是考虑马力性能的&#xff0c;也是敢于标出自己的马力参数数据&#xff0c;就比如印第安的FTR系列。 以泥地赛道为灵感设计的印第安FTR运动街车发布了最新的限量联名款车型&…

spring cloud alibaba 简介

微服务搭建组件选型 1.服务注册中心 Nacos(spring-cloud-alibaba) 2.服务通信 OpenFeign(spring-cloud) 3.服务熔断、降级、限流 Sentinel(spring-cloud-alibaba) 4.网关 Gateway(spring-cloud) 5.服务配置中心 …

ARM64 linux并发与同步之经典自旋锁

1.3 经典自旋锁 在实际项目中临界区数据有可能会修改一个数据结构或者链表中的数据&#xff0c;在整个过程中要保证原子性&#xff0c;才不会影响数据的有效性&#xff0c;这个过程使用原子变量不合适&#xff0c;需要使用锁机制来完成&#xff0c;自旋锁&#xff08;spinlock&…