Files
lgp-admin-plus/packages/effects/common-ui/src/components/captcha/point-selection-captcha/index.vue
xingyu a4736a49f8 feat: migrate to Tailwind CSS v4 (#7614)
* chore: update deps

* feat: use jsonc/x language

* chore: update eslint 10.0

* fix: no-useless-assignment

* feat: add CLAUDE.md

* chore: ignore

* feat: claude

* fix: lint

* chore: suppot eslint v10

* fix: lint

* fix: lint

* fix: type check

* fix: unit test

* fix: Suggested fix

* fix: unit test

* chore: update stylelint v17

* chore: update all major deps

* fix:  echarts console warn

* chore: update vitest v4

* feat: add skills ignores

* chore: update deps

* chore: update deps

* fix: cspell

* chore: update deps

* chore: update tailwindcss v4

* chore: remove postcss config

* fix: no use catalog

* chore: tailwind v4 config

* fix: tailwindcss v4 sort

* feat: use eslint-plugin-better-tailwindcss

* fix: Interference between enforce-consistent-line-wrapping, jsx-curly-brace-presence and Prettier

* fix: Interference between enforce-consistent-line-wrapping, jsx-curly-brace-presence and Prettier

* fix(lint): resolve prettier and better-tailwindcss formatting conflicts

* fix(tailwind): update theme references and lint sources

* style(format): normalize apps docs and playground vue files

* style(format): normalize core ui-kit components

* style(format): normalize effects ui and layout components
2026-03-10 05:08:45 +08:00

177 lines
4.2 KiB
Vue

<script setup lang="ts">
import type { CaptchaPoint, PointSelectionCaptchaProps } from '../types';
import { RotateCw } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';
import { useCaptchaPoints } from '../hooks/useCaptchaPoints';
import CaptchaCard from './point-selection-captcha-card.vue';
const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
height: '220px',
hintImage: '',
hintText: '',
paddingX: '12px',
paddingY: '16px',
showConfirm: false,
title: '',
width: '300px',
});
const emit = defineEmits<{
click: [CaptchaPoint];
confirm: [Array<CaptchaPoint>, clear: () => void];
refresh: [];
}>();
const { addPoint, clearPoints, points } = useCaptchaPoints();
if (!props.hintImage && !props.hintText) {
console.warn('At least one of hint image or hint text must be provided');
}
const POINT_OFFSET = 11;
function getElementPosition(element: HTMLElement) {
const rect = element.getBoundingClientRect();
return {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
};
}
function handleClick(e: MouseEvent) {
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.clientX + window.scrollX;
const mouseY = e.clientY + window.scrollY;
if (typeof mouseX !== 'number' || typeof mouseY !== 'number') {
throw new TypeError('Mouse coordinates not found');
}
const xPos = mouseX - domX;
const yPos = mouseY - domY;
const rect = dom.getBoundingClientRect();
// 点击位置边界校验
if (xPos < 0 || yPos < 0 || xPos > rect.width || yPos > rect.height) {
console.warn('Click position is out of the valid range');
return;
}
const x = Math.ceil(xPos);
const y = Math.ceil(yPos);
const point = {
i: points.length,
t: Date.now(),
x,
y,
};
addPoint(point);
emit('click', point);
e.stopPropagation();
e.preventDefault();
} catch (error) {
console.error('Error in handleClick:', error);
}
}
function clear() {
try {
clearPoints();
} catch (error) {
console.error('Error in clear:', error);
}
}
function handleRefresh() {
try {
clear();
emit('refresh');
} catch (error) {
console.error('Error in handleRefresh:', error);
}
}
function handleConfirm() {
if (!props.showConfirm) return;
try {
emit('confirm', points, clear);
} catch (error) {
console.error('Error in handleConfirm:', error);
}
}
</script>
<template>
<CaptchaCard
:captcha-image="captchaImage"
:height="height"
:padding-x="paddingX"
:padding-y="paddingY"
:title="title"
:width="width"
@click="handleClick"
>
<template #title>
<slot name="title">{{ $t('ui.captcha.title') }}</slot>
</template>
<template #extra>
<VbenIconButton
:aria-label="$t('ui.captcha.refreshAriaLabel')"
class="ml-1"
@click="handleRefresh"
>
<RotateCw class="size-5" />
</VbenIconButton>
<VbenButton
v-if="showConfirm"
:aria-label="$t('ui.captcha.confirmAriaLabel')"
class="ml-2"
size="sm"
@click="handleConfirm"
>
{{ $t('ui.captcha.confirm') }}
</VbenButton>
</template>
<div
v-for="(point, index) in points"
:key="index"
:aria-label="$t('ui.captcha.pointAriaLabel') + (index + 1)"
:style="{
top: `${point.y - POINT_OFFSET}px`,
left: `${point.x - POINT_OFFSET}px`,
}"
class="absolute z-20 flex-center size-5 cursor-default rounded-full border-2 border-primary-50 bg-primary text-primary-50"
role="button"
tabindex="0"
>
{{ index + 1 }}
</div>
<template #footer>
<img
v-if="hintImage"
:alt="$t('ui.captcha.alt')"
:src="hintImage"
class="h-10 w-full rounded-sm border border-border"
/>
<div
v-else-if="hintText"
class="flex-center h-10 w-full rounded-sm border border-border"
>
{{ `${$t('ui.captcha.clickInOrder')}` + `【${hintText}】` }}
</div>
</template>
</CaptchaCard>
</template>