2024-08-08 15:17:44 +08:00

415 lines
10 KiB
Vue

<!--手势密码-->
<script setup lang="ts">
import {GesturePoint, Point} from "@/types";
import {isInCircle} from "@/utils/global.ts";
import {usePasswordStore} from "@/stores/PasswordStore.ts";
import {md5} from "@/utils/security.ts";
const passwordStore = usePasswordStore();
// 声明此组件可能调用的事件
const emits = defineEmits(['complete', 'pass', 'fail'])
let props = defineProps({
// 验证字符串
ciphertext: {
type: String
},
// 显示手势
showGesture: {
type: Boolean,
default: true
}
})
// 全部的点
const allPointArray: Array<GesturePoint> = reactive([])
// 手势划过的点
const passPointArray: Array<GesturePoint> = reactive([])
// 画板div Ref
const gestureDivRef: Ref<HTMLDivElement | undefined> = ref()
// 画板Ref
const canvasRef: Ref<HTMLCanvasElement | undefined> = ref()
// 画笔
let ctx: CanvasRenderingContext2D | undefined = undefined
// 鼠标/手指是否按下
const pressed = ref(false)
// 验证状态
const verifyStatus: Ref<string> = ref('')
// 当前鼠标/手指悬浮的位置
const hoverPoint: Ref<Point | null> = ref(null)
// 组件配置
const config = {
// 每行/每列手势点数量
pointCellSize: 3,
// 手势点在其所在区域的占比
cellPointRadio: 0.33,
// 手势点颜色
gesturePointColor: '#cacaca',
// 手势点错误时的颜色
gesturePointErrorColor: '#F56C6C',
// 圆心占比
circleCenterRadio: 0.25,
// 手势点中心颜色
gesturePointCenterColor: '#409EFF',
// 手势点中心错误时的颜色
gesturePointCenterErrorColor: '#F56C6C',
// 线条宽度
lineWidth: 12,
// 线条颜色
lineColor: 'rgba(64,158,255,0.32)',
// 线条错误时的颜色
lineErrorColor: 'rgba(245,108,108,0.32)',
}
// 手势按下
const canvasDown = (_point: Point) => {
drawCanvas()
}
// 手势移动
const canvasMove = (_point: Point) => {
drawCanvas()
}
// 手势抬起
const canvasUp = () => {
let selectPoint = passPointArray.map((point: GesturePoint) => point.id).join()
verifyPassword(selectPoint)
drawCanvas()
}
// 验证
const verifyPassword = (mainPassword: string) => {
if (!props.ciphertext) {
// 清除手势点
passPointArray.length = 0
if (mainPassword.length < 5) {
console.log('设置密码,手势主密码 请至少连接3个点')
ElMessage.warning('请至少连接3个点')
return;
}
// 验证方法不存在直接完成
emits('complete', md5(mainPassword))
return
}
// 验证密码
verifyStatus.value = passwordStore.passwordManager.verifyPassword(mainPassword, props.ciphertext) ? 'pass' : 'fail'
// 重绘
drawCanvas()
// 验证状态通知
if (verifyStatus.value) {
emits('pass', md5(mainPassword))
} else {
emits('fail', md5(mainPassword))
}
// 验证密码状态0.5秒后清除
setTimeout(() => {
// 清除验证状态
verifyStatus.value = ''
// 清除手势点
passPointArray.length = 0
// 重绘
drawCanvas()
}, 500)
}
// 绘制画板
const drawCanvas = () => {
if (!gestureDivRef.value || !canvasRef.value) return
// 计算经过点
calcPassPoint()
// 清空画板
clearCanvas()
// 绘制手势点
drawGesturePoint()
if (props.showGesture) {
// 绘制鼠标悬浮效果
drawHover();
// 绘制手势经过的点
drawPassPoint()
// 绘制各点连接线
drawLine()
}
}
// 绘制连接线
const drawLine = () => {
if (!ctx) return
let points: Array<Point> = [...passPointArray]
// 当鼠标悬浮且鼠标按下且状态不存在,添加当前鼠标点到轨迹中
if (hoverPoint.value && pressed.value && !verifyStatus.value) {
points.push(hoverPoint.value)
}
if (points.length < 2) {
return
}
for (let i = 1; i < points.length; i++) {
let startPoint: Point = points[i - 1]
let endPoint: Point = points[i]
// 绘制连接线
ctx.beginPath();
ctx.strokeStyle = verifyStatus.value === 'fail' ? config.lineErrorColor : config.lineColor
ctx.lineWidth = config.lineWidth
ctx.moveTo(startPoint.x, startPoint.y);
ctx.lineTo(endPoint.x, endPoint.y)
ctx.stroke();
}
}
// 计算经过点
const calcPassPoint = () => {
// 鼠标按下且校验状态为空时校验
if (!pressed.value || verifyStatus.value === 'fail') return
// 找出鼠标所在点
let gesturePoint = fundHoverGesturePoint()
// 不存在无需计算
if (!gesturePoint) return
// 判断该点是否存在经过列表中
let existPoint = passPointArray.find((pass: GesturePoint) => pass.id === gesturePoint.id)
if (!existPoint) {
passPointArray.push(gesturePoint)
}
}
// 清空画板
const clearCanvas = () => {
if (!ctx || !canvasRef.value) return
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
}
// 绘制手势经过的点
const drawPassPoint = () => {
if (!gestureDivRef.value || !canvasRef.value || !ctx) return
for (let i = 0; i < passPointArray.length; i++) {
let point: GesturePoint = passPointArray[i]
drawCircleCenter(point)
}
}
// 绘制鼠标悬浮效果
const drawHover = () => {
if (!gestureDivRef.value || !canvasRef.value || !ctx) return
// 有验证状态或鼠标未悬浮不绘制
if (verifyStatus.value || !hoverPoint.value) return
let gesturePoint = fundHoverGesturePoint()
if (gesturePoint) {
drawCircleCenter(gesturePoint)
}
}
// 查找鼠标/手势当前所在点
const fundHoverGesturePoint = (): GesturePoint | null => {
if (!gestureDivRef.value || !canvasRef.value || !ctx || !hoverPoint.value) return null
for (let i = 0; i < allPointArray.length; i++) {
let gesturePoint: GesturePoint = allPointArray[i]
// 判断鼠标是否在圆形区域
let inCircle = isInCircle(gesturePoint.x, gesturePoint.y, gesturePoint.radius, hoverPoint.value.x, hoverPoint.value?.y)
if (inCircle) {
return gesturePoint
}
}
return null
}
// 画触点圆心
const drawCircleCenter = (gesturePoint: GesturePoint) => {
if (!ctx) return
// 绘制外圈
ctx.beginPath();
ctx.fillStyle = verifyStatus.value ? config.gesturePointCenterErrorColor : config.gesturePointCenterColor
ctx.arc(gesturePoint.x, gesturePoint.y, gesturePoint.radius * config.circleCenterRadio, 0, Math.PI * 2);
ctx.fill();
// 绘制圆心
ctx.beginPath();
ctx.fillStyle = verifyStatus.value ? config.lineErrorColor : config.lineColor
ctx.arc(gesturePoint.x, gesturePoint.y, gesturePoint.radius * config.circleCenterRadio * 2, 0, Math.PI * 2);
ctx.fill();
// 绘制半透明外围
ctx.beginPath();
ctx.strokeStyle = verifyStatus.value ? config.gesturePointCenterErrorColor : config.gesturePointCenterColor
ctx.lineWidth = 2
ctx.arc(gesturePoint.x, gesturePoint.y, gesturePoint.radius, 0, Math.PI * 2);
ctx.stroke();
}
// 绘制手势点
const drawGesturePoint = () => {
if (!ctx) return
allPointArray.forEach((gesturePoint: GesturePoint) => {
if (!ctx) return
// 绘制手势点
ctx.beginPath();
ctx.strokeStyle = config.gesturePointColor
ctx.lineWidth = 2
ctx.arc(gesturePoint.x, gesturePoint.y, gesturePoint.radius, 0, Math.PI * 2);
ctx.stroke();
// 绘制手势点中心
ctx.beginPath();
ctx.fillStyle = config.gesturePointColor
ctx.arc(gesturePoint.x, gesturePoint.y, gesturePoint.radius * config.circleCenterRadio, 0, Math.PI * 2);
ctx.fill();
})
}
// 鼠标按下
const mousedown = (e: MouseEvent) => {
pressed.value = true
let point = {x: e.offsetX, y: e.offsetY}
hoverPoint.value = point
canvasDown(point)
}
// 鼠标移动
const mousemove = (e: MouseEvent) => {
let point = {x: e.offsetX, y: e.offsetY}
hoverPoint.value = point
canvasMove(point)
}
// 鼠标抬起
const mouseup = () => {
if (!pressed.value) return
pressed.value = false
hoverPoint.value = null
canvasUp()
}
// 鼠标离开
const mouseleave = () => {
if (!pressed.value) return
pressed.value = false
hoverPoint.value = null
canvasUp()
}
// 手指按下
const touchstart = (e: TouchEvent) => {
if (!canvasRef.value) return
pressed.value = true
let rect = canvasRef.value.getBoundingClientRect()
let point: Point = {
x: e.touches[0].clientX - rect.x,
y: e.touches[0].clientY - rect.y
}
hoverPoint.value = point
canvasDown(point)
}
// 手指移动
const touchmove = (e: TouchEvent) => {
if (!canvasRef.value) return
let rect = canvasRef.value.getBoundingClientRect()
let point: Point = {
x: e.touches[0].clientX - rect.x,
y: e.touches[0].clientY - rect.y
}
hoverPoint.value = point
canvasMove(point)
}
// 手指抬起
const touchend = () => {
if (!pressed.value) return
pressed.value = false
hoverPoint.value = null
canvasUp()
}
function createHDCanvas(canvas: any, w: number, h: number) {
const ratio = window.devicePixelRatio || 1;
canvas.width = w * ratio; // 实际渲染像素
canvas.height = h * ratio; // 实际渲染像素
canvas.style.width = `${w}px`; // 控制显示大小
canvas.style.height = `${h}px`; // 控制显示大小
const ctx = canvas.getContext('2d')
ctx.scale(ratio, ratio)
return ctx;
}
// 初始化画板
const initCanvas = () => {
if (!gestureDivRef.value || !canvasRef.value) return
let size = gestureDivRef.value.clientWidth
console.log('画板大小:', size)
canvasRef.value.width = size
canvasRef.value.height = size
ctx = createHDCanvas(canvasRef.value, size, size) as CanvasRenderingContext2D
let incrId = 0
allPointArray.length = 0
// 计算手势点位置
for (let i = 0; i < config.pointCellSize; i++) {
for (let j = 0; j < config.pointCellSize; j++) {
let x = i * size / config.pointCellSize
let y = j * size / config.pointCellSize
let w = size / config.pointCellSize
let h = size / config.pointCellSize
let gesturePoint: GesturePoint = {
id: incrId++,
x: x + w / 2,
y: y + h / 2,
radius: w * config.cellPointRadio,
}
allPointArray.push(gesturePoint)
}
}
drawCanvas()
}
let resizeTimeout: any = null
window.onresize = () => {
if (resizeTimeout) {
clearTimeout(resizeTimeout)
}
resizeTimeout = setTimeout(initCanvas, 100);
}
onMounted(() => {
nextTick(() => {
initCanvas()
})
})
</script>
<template>
<div ref="gestureDivRef" style="width: 100%;height: 100%;">
<canvas
ref="canvasRef"
style="image-rendering: auto;"
@mousedown="mousedown"
@mousemove="mousemove"
@mouseup="mouseup"
@mouseleave="mouseleave"
@touchstart="touchstart"
@touchmove="touchmove"
@touchend="touchend"
></canvas>
</div>
</template>
<style scoped>
</style>