-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: captcha example * fix: fix lint errors * chore: event handling and methods * chore: add accessibility features ARIA labels and roles --------- Co-authored-by: vince <vince292007@gmail.com>
- Loading branch information
1 parent
ad89ea7
commit b163640
Showing
10 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export { default as PointSelectionCaptcha } from './point-selection-captcha.vue'; | ||
export interface Point { | ||
i: number; | ||
x: number; | ||
y: number; | ||
t: number; | ||
} | ||
export type ClearFunction = () => void; |
241 changes: 241 additions & 0 deletions
241
packages/effects/common-ui/src/components/captcha/point-selection-captcha.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
<script setup lang="ts"> | ||
import { computed, ref } from 'vue'; | ||
import { VbenButton } from '@vben/common-ui'; | ||
import { SvgRefreshIcon } from '@vben/icons'; | ||
import { | ||
Card, | ||
CardContent, | ||
CardFooter, | ||
CardHeader, | ||
CardTitle, | ||
VbenIconButton, | ||
} from '@vben-core/shadcn-ui'; | ||
import { type Point } from '.'; | ||
interface Props { | ||
/** | ||
* 点选的图片 | ||
* @default '12px' | ||
*/ | ||
captchaImage: string; | ||
/** | ||
* 验证码图片高度 | ||
* @default '220px' | ||
*/ | ||
height?: number | string; | ||
/** | ||
* 提示图片高度 | ||
* @default '40px' | ||
*/ | ||
hintHeight?: number | string; | ||
/** | ||
* 提示图片宽度 | ||
* @default '150px' | ||
*/ | ||
hintWidth?: number | string; | ||
/** | ||
* 提示图片 | ||
* @default '12px' | ||
*/ | ||
hintImage: string; | ||
/** | ||
* 水平内边距 | ||
* @default '12px' | ||
*/ | ||
paddingX?: number | string; | ||
/** | ||
* 垂直内边距 | ||
* @default '16px' | ||
*/ | ||
paddingY?: number | string; | ||
/** | ||
* 标题 | ||
* @default '请按图依次点击' | ||
*/ | ||
title?: string; | ||
/** | ||
* 验证码图片宽度 | ||
* @default '300px' | ||
*/ | ||
width?: number | string; | ||
} | ||
const props = withDefaults(defineProps<Props>(), { | ||
height: '220px', | ||
hintHeight: '40px', | ||
hintWidth: '150px', | ||
paddingX: '12px', | ||
paddingY: '16px', | ||
title: '请按图依次点击', | ||
width: '300px', | ||
}); | ||
const emit = defineEmits<{ | ||
click: [number, number]; | ||
confirm: [Array<Point>, clear: () => void]; | ||
refresh: []; | ||
}>(); | ||
const parseValue = (value: number | string) => { | ||
if (typeof value === 'number') { | ||
return value; | ||
} | ||
const parsed = Number.parseFloat(value); | ||
return Number.isNaN(parsed) ? 0 : parsed; | ||
}; | ||
const rootStyles = computed(() => ({ | ||
padding: `${parseValue(props.paddingY)}px ${parseValue(props.paddingX)}px`, | ||
width: `${parseValue(props.width) - parseValue(props.paddingX) * 2}px`, | ||
})); | ||
const hintStyles = computed(() => ({ | ||
height: `${parseValue(props.hintHeight)}px`, | ||
width: `${parseValue(props.hintWidth)}px`, | ||
})); | ||
const captchaStyles = computed(() => { | ||
return { | ||
height: `${parseValue(props.height)}px`, | ||
width: `${parseValue(props.width)}px`, | ||
}; | ||
}); | ||
function getElementPosition(element: HTMLElement) { | ||
let posX = 0; | ||
let posY = 0; | ||
if (element.getBoundingClientRect) { | ||
const rect = element.getBoundingClientRect(); | ||
const doc = document.documentElement; | ||
posX = | ||
rect.left + | ||
Math.max(doc.scrollLeft, document.body.scrollLeft) - | ||
doc.clientLeft; | ||
posY = | ||
rect.top + | ||
Math.max(doc.scrollTop, document.body.scrollTop) - | ||
doc.clientTop; | ||
} else { | ||
while (element !== document.body) { | ||
posX += element.offsetLeft; | ||
posY += element.offsetTop; | ||
element = element.offsetParent as HTMLElement; | ||
} | ||
} | ||
return { | ||
x: posX, | ||
y: posY, | ||
}; | ||
} | ||
const points = ref<Point[]>([]); | ||
const POINT_OFFSET = 11; | ||
function handleClick(e: any | Event) { | ||
try { | ||
const dom = e.currentTarget as HTMLElement; | ||
if (!dom) throw new Error('Element not found'); | ||
const { x: domX, y: domY } = getElementPosition(dom); | ||
const mouseX = e.pageX || e.clientX; | ||
const mouseY = e.pageY || e.clientY; | ||
if (mouseX === undefined || mouseY === undefined) | ||
throw new Error('Mouse coordinates not found'); | ||
const xPos = mouseX - domX; | ||
const yPos = mouseY - domY; | ||
const x = Math.ceil(xPos); | ||
const y = Math.ceil(yPos); | ||
points.value.push({ | ||
i: points.value.length, | ||
t: Date.now(), | ||
x, | ||
y, | ||
}); | ||
emit('click', x, y); | ||
e.cancelBubble = true; | ||
e.preventDefault(); | ||
} catch (error) { | ||
console.error('Error in handleClick:', error); | ||
} | ||
} | ||
function clear() { | ||
try { | ||
points.value = []; | ||
} catch (error) { | ||
console.error('Error in clear:', error); | ||
} | ||
} | ||
function handleRefresh() { | ||
try { | ||
clear(); | ||
emit('refresh'); | ||
} catch (error) { | ||
console.error('Error in handleRefresh:', error); | ||
} | ||
} | ||
function handleConfirm() { | ||
try { | ||
emit('confirm', points.value, clear); | ||
} catch (error) { | ||
console.error('Error in handleConfirm:', error); | ||
} | ||
} | ||
</script> | ||
<template> | ||
<Card :style="rootStyles" aria-labelledby="captcha-title" role="region"> | ||
<CardHeader class="p-0"> | ||
<CardTitle id="captcha-title" class="flex items-center justify-between"> | ||
<span>{{ title }}</span> | ||
<img | ||
v-show="hintImage" | ||
:src="hintImage" | ||
:style="hintStyles" | ||
alt="提示图片" | ||
/> | ||
</CardTitle> | ||
</CardHeader> | ||
<CardContent class="relative mt-2 flex w-full overflow-hidden rounded p-0"> | ||
<img | ||
v-show="captchaImage" | ||
:src="captchaImage" | ||
:style="captchaStyles" | ||
alt="验证码图片" | ||
class="relative z-10" | ||
@click="handleClick" | ||
/> | ||
<div class="absolute inset-0"> | ||
<div | ||
v-for="(point, index) in points" | ||
:key="index" | ||
:style="{ | ||
top: `${point.y - POINT_OFFSET}px`, | ||
left: `${point.x - POINT_OFFSET}px`, | ||
}" | ||
aria-label="点击点 {{ index + 1 }}" | ||
class="bg-primary text-primary-50 border-primary-50 absolute z-20 flex h-5 w-5 cursor-default items-center justify-center rounded-full border-2" | ||
role="button" | ||
> | ||
{{ index + 1 }} | ||
</div> | ||
</div> | ||
</CardContent> | ||
<CardFooter class="mt-2 flex justify-between p-0"> | ||
<VbenIconButton aria-label="刷新验证码" @click="handleRefresh"> | ||
<SvgRefreshIcon class="size-6" /> | ||
</VbenIconButton> | ||
<VbenButton aria-label="确认选择" @click="handleConfirm"> | ||
确认 | ||
</VbenButton> | ||
</CardFooter> | ||
</Card> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,6 +71,9 @@ | |
}, | ||
"ellipsis": { | ||
"title": "EllipsisText" | ||
}, | ||
"captcha": { | ||
"title": "Captcha" | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,6 +71,9 @@ | |
}, | ||
"ellipsis": { | ||
"title": "文本省略" | ||
}, | ||
"captcha": { | ||
"title": "验证码" | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<script lang="ts" setup> | ||
import { ref } from 'vue'; | ||
import { Page, type Point, PointSelectionCaptcha } from '@vben/common-ui'; | ||
import { Card } from 'ant-design-vue'; | ||
import { captchaImage, hintImage } from './base64'; | ||
const selectedPoints = ref<Point[]>([]); | ||
const handleConfirm = (points: Point[], clear: () => void) => { | ||
selectedPoints.value = points; | ||
clear(); | ||
}; | ||
const handleRefresh = () => { | ||
selectedPoints.value = []; | ||
}; | ||
</script> | ||
|
||
<template> | ||
<Page | ||
description="通过点击图片中的特定位置来验证用户身份。" | ||
title="验证码组件示例" | ||
> | ||
<Card class="mb-4" title="基本使用"> | ||
<PointSelectionCaptcha | ||
:captcha-image="captchaImage" | ||
:hint-image="hintImage" | ||
class="float-left" | ||
@confirm="handleConfirm" | ||
@refresh="handleRefresh" | ||
/> | ||
<div class="float-left p-5"> | ||
<div v-for="point in selectedPoints" :key="point.i" class="flex"> | ||
<span class="mr-3 w-16">索引:{{ point.i }}</span> | ||
<span class="mr-3 w-44">时间戳:{{ point.t }}</span> | ||
<span class="mr-3 w-16">x:{{ point.x }}</span> | ||
<span class="mr-3 w-16">y:{{ point.y }}</span> | ||
</div> | ||
</div> | ||
</Card> | ||
</Page> | ||
</template> |