php+vue3實(shí)現(xiàn)點(diǎn)選驗(yàn)證碼功能
buildadmin 中的點(diǎn)選驗(yàn)證碼實(shí)現(xiàn)
驗(yàn)證碼類
<?php
namespace ba;
use Throwable;
use think\facade\Db;
use think\facade\Lang;
use think\facade\Config;
/**
* 點(diǎn)選文字驗(yàn)證碼類
*/
class ClickCaptcha
{
/**
* 驗(yàn)證碼過期時(shí)間(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',
];
/**
* 驗(yàn)證點(diǎn) Icon 映射表
* @var array
*/
private array $iconDict = [
'aeroplane' => '飛機(jī)',
'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' => '們以我到他會(huì)作時(shí)要?jiǎng)訃a(chǎn)的是工就年階義發(fā)成部民可出能方進(jìn)在和有大這主中為來分生對(duì)于學(xué)級(jí)地用同行面說種過命度革而多子后自社加小機(jī)也經(jīng)力線本電高量長黨得實(shí)家定深法表著水理化爭(zhēng)現(xiàn)所起政好十戰(zhàn)無農(nóng)使前等反體合斗路圖把結(jié)第里正新開論之物從當(dāng)兩些還天資事隊(duì)點(diǎn)育重其思與間內(nèi)去因件利相由壓?jiǎn)T氣業(yè)代全組數(shù)果期導(dǎo)平各基或月然如應(yīng)形想制心樣都向變關(guān)問比展那它最及外沒看治提五解系林者米群頭意只明四道馬認(rèn)次文通但條較克又公孔領(lǐng)軍流接席位情運(yùn)器并飛原油放立題質(zhì)指建區(qū)驗(yàn)活眾很教決特此常石強(qiáng)極已根共直團(tuán)統(tǒng)式轉(zhuǎn)別造切九你取西持總料連任志觀調(diào)么山程百報(bào)更見必真保熱委手改管處己將修支識(shí)象先老光專什六型具示復(fù)安帶每東增則完風(fēng)回南勞輪科北打積車計(jì)給節(jié)做務(wù)被整聯(lián)步類集號(hào)列溫裝即毫知軸研單堅(jiān)據(jù)速防史拉世設(shè)達(dá)爾場(chǎng)織歷花求傳斷況采精金界品判參層止邊清至萬確究書術(shù)狀須離再目海權(quán)且青才證低越際八試規(guī)斯近注辦布門鐵需走議縣兵固除般引齒勝細(xì)影濟(jì)白格效置推空配葉率述今選養(yǎng)德話查差半敵始片施響收華覺備名紅續(xù)均藥標(biāo)記難存測(cè)身緊液派準(zhǔn)斤角降維板許破述技消底床田勢(shì)端感往神便賀村構(gòu)照容非亞磨族段算適講按值美態(tài)易彪服早班麥削信排臺(tái)聲該擊素張密害侯何樹肥繼右屬市嚴(yán)徑螺檢左頁抗蘇顯苦英快稱壞移巴材省黑武培著河帝僅針怎植京助升王眼她抓苗副雜普談圍食源例致酸舊卻充足短劃劑宣環(huán)落首尺波承粉踐府魚隨考刻靠夠滿夫失包住促枝局菌桿周護(hù)巖師舉曲春元超負(fù)砂封換太模貧減陽揚(yáng)江析畝木言球朝醫(yī)校古呢稻宋聽唯輸滑站另衛(wèi)字鼓剛寫劉微略范供阿塊某功友限項(xiàng)余倒卷創(chuàng)律雨讓骨遠(yuǎn)幫初皮播優(yōu)占圈偉季訓(xùn)控激找叫云互跟糧粒母練塞鋼頂策雙留誤礎(chǔ)阻故寸盾晚絲女散焊功株親院冷徹彈錯(cuò)散商視藝版烈零室輕倍缺厘泵察絕富城沖噴壤簡(jiǎn)否柱李望盤磁雄似困鞏益洲脫投送側(cè)潤蓋揮距觸星松送獲興獨(dú)官混紀(jì)依未突架寬冬章偏紋吃執(zhí)閥礦寨責(zé)熟穩(wěn)奪硬價(jià)努翻奇甲預(yù)職評(píng)讀背協(xié)損棉侵灰雖矛厚羅泥辟告箱掌氧恩愛停曾溶營終綱孟錢待盡俄縮沙退陳討奮械載胞哪旋征槽倒握擔(dān)仍呀鮮吧卡粗介鉆逐弱腳怕鹽末豐霧冠丙街萊貝輻腸付吉滲瑞驚頓擠秒懸姆森糖圣凹陶詞遲蠶億矩康遵牧遭幅園腔訂香肉弟屋敏恢忘編印蜂急拿擴(kuò)飛露核緣游振操央伍域甚迅輝異序免紙夜鄉(xiāng)久隸念蘭映溝乙嗎儒汽磷艱晶埃燃?xì)g鐵補(bǔ)咱芽永瓦傾陣碳演威附牙芽永瓦斜灌歐獻(xiàn)順豬洋腐請(qǐng)透司括脈宜笑若尾束壯暴企菜穗楚漢愈綠拖牛份染既秋遍鍛玉夏療尖井費(fèi)州訪吹榮銅沿替滾客召旱悟刺腦措貫藏敢令隙爐殼硫煤迎鑄粘探臨薄旬善??v擇禮愿伏殘雷延煙句純漸耕跑澤慢栽魯赤繁境潮橫掉錐希池?cái)〈倭林^托伙哲懷擺貢呈勁財(cái)儀沉煉麻祖息車穿貨銷齊鼠抽畫飼龍庫守筑房歌寒喜哥洗蝕廢納腹乎錄鏡脂莊擦險(xiǎn)贊鐘搖典柄辯竹谷亂虛橋奧伯趕垂途額壁網(wǎng)截野遺靜謀弄掛課鎮(zhèn)妄盛耐扎慮鍵歸符慶聚繞摩忙舞遇索顧膠羊湖釘仁音跡碎伸燈避泛答勇頻皇柳哈揭甘諾概憲濃島襲誰洪謝炮澆斑訊懂靈蛋閉孩釋巨徒私銀伊景坦累勻霉杜樂勒隔彎績(jī)招紹胡呼峰零柴簧午跳居尚秦稍追梁折耗堿殊崗?fù)谑先袆《押蘸尚睾馇谀て邱v案刊秧緩?fù)挂奂舸ㄑ╂湞O啦臉戶洛孢勃盟買楊宗焦賽旗濾硅炭股坐蒸凝竟槍黎救冒暗洞犯筒您宋弧爆謬涂味津臂障褐陸啊健尊豆拔莫抵桑坡縫警挑冰柬嘴啥飯塑寄趙喊墊丹渡耳虎筆稀昆浪薩茶滴淺擁覆倫娘噸浸袖珠雌媽紫戲塔錘震歲貌潔剖牢鋒疑霸閃埔猛訴刷忽鬧喬唐漏聞沈熔氯荒莖男凡搶像漿旁玻亦忠唱蒙予紛捕鎖尤乘烏智淡允叛畜俘摸銹掃畢璃寶芯爺鑒秘凈蔣鈣肩騰枯拋軌堂拌爸循誘祝勵(lì)肯酒繩塘燥泡袋朗喂鋁軟渠顆慣貿(mào)綜墻趨彼屆墨礙啟逆卸航衣孫齡嶺休借',
];
/**
* 構(gòu)造方法
* @param array $config 點(diǎn)擊驗(yàn)證碼配置
* @throws Throwable
*/
public function __construct(array $config = [])
{
$clickConfig = Config::get('buildadmin.click_captcha');
//這里會(huì)得配置文件中的數(shù)據(jù)合并
//$clickConfig 中的配置是這樣的
// 'click_captcha' => [
// 模式:text=文字,icon=圖標(biāo)(若只有icon則適用于國際化站點(diǎn))
//'mode' => ['text', 'icon'],
// 長度
//'length' => 2,
// 混淆點(diǎn)長度
//'confuse_length' => 2,
],
$this->config = array_merge($clickConfig, $this->config, $config);
// 清理過期的驗(yàn)證碼
Db::name('captcha')->where('expire_time', '<', time())->delete();
}
/**
* 創(chuàng)建圖形驗(yàn)證碼
* @param string $id 驗(yàn)證碼ID,開發(fā)者自定義
* @return array 返回驗(yàn)證碼圖片的base64編碼和驗(yàn)證碼文字信息
*/
public function creat(string $id): array
{
$imagePath = Filesystem::fsFit(public_path() . $this->bgPaths[mt_rand(0, count($this->bgPaths) - 1)]); //隨機(jī)一個(gè)背景圖片
$fontPath = Filesystem::fsFit(public_path() . $this->fontPaths[mt_rand(0, count($this->fontPaths) - 1)]); //隨機(jī)一個(gè)字體
$randPoints = $this->randPoints($this->config['length'] + $this->config['confuse_length']); //生成驗(yàn)證碼的長度, 加上混肖點(diǎn)的長度相加
$lang = Lang::getLangSet();
foreach ($randPoints as $v) {
$tmp['size'] = rand(15, 30);
if (isset($this->iconDict[$v])) {
// 圖標(biāo)
$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 {
// 字符串文本框?qū)挾群烷L度
$fontArea = imagettfbbox($tmp['size'], 0, $fontPath, $v);
$textWidth = $fontArea[2] - $fontArea[0]; //得到文字的寬度
$textHeight = $fontArea[1] - $fontArea[7]; //得到文字的高度
$tmp['icon'] = false; //說明這個(gè)不是圖片
$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 的高度 是背景圖高度
// 隨機(jī)生成驗(yàn)證點(diǎn)位置
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']; //這里的把生成的標(biāo)記也按順序 記錄了下來
}
unset($v);
;
// 創(chuàng)建圖片的實(shí)例
$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']); //取了兩個(gè)
$text = array_splice($text, 0, $this->config['length']); //前兩個(gè)的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'],
];
}
/**
* 檢查驗(yàn)證碼
* @param string $id 開發(fā)者自定義的驗(yàn)證碼ID
* @param string $info 驗(yàn)證信息
* @param bool $unset 驗(yàn)證成功是否刪除驗(yàn)證碼
* @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) {
// 驗(yàn)證碼過期
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);
}
/**
* 隨機(jī)生成驗(yàn)證點(diǎn)元素
* @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 個(gè)文字
// 圖標(biāo)
if (in_array('icon', $this->config['mode'])) {
$icon = array_keys($this->iconDict); //得到所有的圖片的 key
shuffle($icon); //打亂key的順序
$icon = array_slice($icon, 0, $length); //截取4個(gè)圖片的key
$arr = array_merge($arr, $icon); //把生成的 文字和圖片的 數(shù)組合并
}
shuffle($arr); //打亂順序
return array_slice($arr, 0, $length); //取出前4個(gè)
}
/**
* 隨機(jī)生成位置布局
* @param array $textArr 點(diǎn)位數(shù)據(jù)
* @param int $imgW 圖片寬度
* @param int $imgH 圖片高度
* @param int $fontW 文字寬度
* @param int $fontH 文字高度
* @param bool $isIcon 是否是圖標(biāo)
* @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);
// 碰撞驗(yàn)證
if (!$this->checkPosition($textArr, $x, $y, $fontW, $fontH, $isIcon)) {
$position = $this->randPosition($textArr, $imgW, $imgH, $fontW, $fontH, $isIcon);
} else {
$position = [$x, $y];
}
return $position;
}
/**
* 碰撞驗(yàn)證
* @param array $textArr 驗(yàn)證點(diǎn)數(shù)據(jù)
* @param int $x x軸位置
* @param int $y y軸位置
* @param int $w 驗(yàn)證點(diǎn)寬度
* @param int $h 驗(yàn)證點(diǎn)高度
* @param bool $isIcon 是否是圖標(biāo)
* @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;
}
}




這里的知識(shí)點(diǎn),和驗(yàn)證的時(shí)候,圖片和文字的 x 坐標(biāo)和 y 坐標(biāo)的對(duì)比不一樣是有關(guān)系的



前端代碼
前端通過代碼, 請(qǐng)求 后臺(tái)的 驗(yàn)證碼的 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>



當(dāng)點(diǎn)擊次數(shù)達(dá)到兩次的時(shí)候,就提交到后臺(tái)去驗(yàn)證
前端提交的數(shù)據(jù)格式是這樣的

id是前后端對(duì)應(yīng)的, info 中 以 “-” 分隔了 兩次點(diǎn)擊的 坐標(biāo)點(diǎn) , 350是圖片的完度,200是圖片的高度
接下來后端進(jìn)行驗(yàn)證, 我們來看看后端的驗(yàn)證過程


自已寫一個(gè)試試, 后端的接口還是用的 buildadmin 的接口,前端我自己寫了一下簡(jiǎn)易代碼做了一下實(shí)驗(yàn),如果開發(fā)的時(shí)候 可以使用上面的 代碼, 注意上面的代碼是 ts 的,稍稍改一下代碼就可以了
以下是我用 vue3 js寫的簡(jiǎn)易代碼, 也是可以實(shí)現(xiàn)驗(yàn)證碼的 , 僅供參考
<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>請(qǐng)點(diǎn)擊字符或圖片:{{data.captchaInfo.text}}</div>
<button @click="getphoto"> 刷新驗(yàn)證碼 </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:"", //后端返回的圖片的寬高, 一般在顯示的時(shí)候就按這個(gè)大小顯示, 本例中沒有使用它們,而是自定義了一個(gè) 寬高,驗(yàn)證時(shí),要把本地自定義的寬高傳給后端才可以
height:"",
id:uuid,
number:0, //當(dāng)前圖片被點(diǎn)擊的次數(shù)
clickposition:"",
clickXY:[]
}
})
//生命周期
onMounted(()=>{
console.log(123);
uuid = Math.floor(Math.random()*(10000-1+1))+1;
getphoto(); //生命周期開始時(shí)調(diào)用后臺(tái)接口,得到 驗(yàn)證碼圖片
});
//圖片的點(diǎn)擊事件
let clickcaptcha = (e)=>{
let xy = e.offsetX+","+e.offsetY; //得到點(diǎn)擊的位置,因?yàn)槭莾蓚€(gè)驗(yàn)證碼,所以要點(diǎn)擊兩次
if(data.captchaInfo.number == 0){
data.captchaInfo.clickposition = xy; //如果是第一次點(diǎn)擊 記錄一下, 點(diǎn)擊位置
data.captchaInfo.clickXY = [{x:e.offsetX,y:e.offsetY}]
}else {
data.captchaInfo.clickposition = data.captchaInfo.clickposition + "-" + xy; //如果是第二次點(diǎn)擊 ,把兩次點(diǎn)擊的位置都記錄下來
data.captchaInfo.clickXY.push({x:e.offsetX,y:e.offsetY})
}
data.captchaInfo.number++;
if(data.captchaInfo.number == 2){ //點(diǎn)擊了兩次
checkClickCaptcha(data.captchaInfo.id, data.captchaInfo.clickposition+";"+'300;200', true)
.then((res) => {
console.log(res);
if(res.code == 1){
//這里驗(yàn)證成功的代碼, 驗(yàn)證成功之后, 把captchaInfo 的數(shù)據(jù)清空
//然后提交表單中的數(shù)據(jù)
alert("驗(yàn)證成功")
}else if(res.code == 0){
alert("驗(yàn)證失敗")
}
})
.catch(() => {
alert("驗(yàn)證失敗")
})
}
}
let getphoto = ()=>{
getCaptchaData(uuid).then(res=>{
data.captchaInfo = Object.assign(data.captchaInfo,res.data,{ number:0, //當(dāng)前圖片被點(diǎn)擊的次數(shù)
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>
到此這篇關(guān)于php+vue3實(shí)現(xiàn)點(diǎn)選驗(yàn)證碼的文章就介紹到這了,更多相關(guān)vue點(diǎn)選驗(yàn)證碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- vue3+vite3+typescript實(shí)現(xiàn)驗(yàn)證碼功能及表單驗(yàn)證效果
- vue3發(fā)送驗(yàn)證碼倒計(jì)時(shí)功能的實(shí)現(xiàn)(防止連點(diǎn)、封裝復(fù)用)
- Vue3?實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)功能
- Vue3?實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)功能(刷新保持狀態(tài))
- Vue3+Vue-cli4項(xiàng)目中使用騰訊滑塊驗(yàn)證碼的方法
- vue3.0實(shí)現(xiàn)點(diǎn)擊切換驗(yàn)證碼(組件)及校驗(yàn)
相關(guān)文章
vue中axios處理http發(fā)送請(qǐng)求的示例(Post和get)
本篇文章主要介紹了vue中axios處理http請(qǐng)求的示例(Post和get),這里整理了詳細(xì)的代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
Vue2 Vue-cli中使用Typescript的配置詳解
Vue作為前端三大框架之一截至到目前在github上以收獲44,873顆星,足以說明其以悄然成為主流。下面這篇文章主要給大家介紹了關(guān)于Vue2 Vue-cli中使用Typescript的配置的相關(guān)資料,需要的朋友可以參考下。2017-07-07
Vue手動(dòng)控制點(diǎn)擊事件Click觸發(fā)方式
這篇文章主要介紹了Vue手動(dòng)控制點(diǎn)擊事件Click觸發(fā)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
Vue?項(xiàng)目中選擇?TSX?而非傳統(tǒng)?.vue?文件的原因分析
文章探討了Vue項(xiàng)目中使用TSX(TypeScript?JSX)的背景、優(yōu)勢(shì)和局限性,TSX結(jié)合了TypeScript和JSX,增強(qiáng)了類型安全和代碼組織性,但也增加了學(xué)習(xí)曲線和可讀性問題,對(duì)于復(fù)雜應(yīng)用,TSX提供了更大的靈活性和類型支持,逐漸成為一些開發(fā)者的選擇2024-11-11
elementUI el-form 數(shù)據(jù)無法賦值且不報(bào)錯(cuò)解決方法
本文主要介紹了elementUI el-form 數(shù)據(jù)無法賦值且不報(bào)錯(cuò)解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-12-12
多個(gè)vue項(xiàng)目實(shí)現(xiàn)共用一個(gè)node-modules文件夾
這篇文章主要介紹了多個(gè)vue項(xiàng)目實(shí)現(xiàn)共用一個(gè)node-modules文件夾,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09

