refactor: refactor code structure and improve demo page (#4389)
* feat: captcha example * fix: fix lint errors * chore: event handling and methods * chore: add accessibility features ARIA labels and roles * refactor: refactor code structure and improve captcha demo page * feat: add captcha internationalization * chore: 适配时间戳国际化展示 --------- Co-authored-by: vince <vince292007@gmail.com>pull/48/MERGE
parent
10b90eae5d
commit
5ba3a9dec2
|
@ -0,0 +1,77 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CaptchaCardProps } from './types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { parseValue } from './utils';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<CaptchaCardProps>(), {
|
||||||
|
height: '220px',
|
||||||
|
paddingX: '12px',
|
||||||
|
paddingY: '16px',
|
||||||
|
title: '',
|
||||||
|
width: '300px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [MouseEvent];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rootStyles = computed(() => ({
|
||||||
|
padding: `${parseValue(props.paddingY)}px ${parseValue(props.paddingX)}px`,
|
||||||
|
width: `${parseValue(props.width) + parseValue(props.paddingX) * 2}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const captchaStyles = computed(() => {
|
||||||
|
return {
|
||||||
|
height: `${parseValue(props.height)}px`,
|
||||||
|
width: `${parseValue(props.width)}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClick(e: MouseEvent) {
|
||||||
|
emit('click', e);
|
||||||
|
}
|
||||||
|
</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">
|
||||||
|
<template v-if="$slots.title">
|
||||||
|
<slot name="title">{{ $t('captcha.title') }}</slot>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</template>
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<slot name="extra"></slot>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="relative mt-2 flex w-full overflow-hidden rounded p-0">
|
||||||
|
<img
|
||||||
|
v-show="captchaImage"
|
||||||
|
:alt="$t('captcha.alt')"
|
||||||
|
:src="captchaImage"
|
||||||
|
:style="captchaStyles"
|
||||||
|
class="relative z-10"
|
||||||
|
@click="handleClick"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-0">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter class="mt-2 flex justify-between p-0">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</template>
|
|
@ -1,2 +1,3 @@
|
||||||
|
export { default as CaptchaCard } from './captcha-card.vue';
|
||||||
export { default as PointSelectionCaptcha } from './point-selection-captcha.vue';
|
export { default as PointSelectionCaptcha } from './point-selection-captcha.vue';
|
||||||
export type * from './types';
|
export type * from './types';
|
||||||
|
|
|
@ -1,107 +1,37 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CaptchaPoint } from './types';
|
import type { CaptchaPoint, PointSelectionCaptchaProps } from './types';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { RotateCw } from '@vben/icons';
|
import { RotateCw } from '@vben/icons';
|
||||||
import {
|
import { $t } from '@vben/locales';
|
||||||
Card,
|
import { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
VbenButton,
|
|
||||||
VbenIconButton,
|
|
||||||
} from '@vben-core/shadcn-ui';
|
|
||||||
|
|
||||||
interface Props {
|
import { CaptchaCard } from '.';
|
||||||
/**
|
|
||||||
* 点选的图片
|
|
||||||
* @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>(), {
|
const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
|
||||||
height: '220px',
|
height: '220px',
|
||||||
hintHeight: '40px',
|
hintImage: '',
|
||||||
hintWidth: '150px',
|
hintText: '',
|
||||||
paddingX: '12px',
|
paddingX: '12px',
|
||||||
paddingY: '16px',
|
paddingY: '16px',
|
||||||
title: '请按图依次点击',
|
showConfirm: false,
|
||||||
|
title: '',
|
||||||
width: '300px',
|
width: '300px',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
click: [number, number];
|
click: [CaptchaPoint];
|
||||||
confirm: [Array<CaptchaPoint>, clear: () => void];
|
confirm: [Array<CaptchaPoint>, clear: () => void];
|
||||||
refresh: [];
|
refresh: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const parseValue = (value: number | string) => {
|
if (!props.hintImage && !props.hintText) {
|
||||||
if (typeof value === 'number') {
|
throw new Error('At least one of hint image or hint text must be provided');
|
||||||
return value;
|
}
|
||||||
}
|
|
||||||
const parsed = Number.parseFloat(value);
|
|
||||||
return Number.isNaN(parsed) ? 0 : parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rootStyles = computed(() => ({
|
const points = ref<CaptchaPoint[]>([]);
|
||||||
padding: `${parseValue(props.paddingY)}px ${parseValue(props.paddingX)}px`,
|
const POINT_OFFSET = 11;
|
||||||
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) {
|
function getElementPosition(element: HTMLElement) {
|
||||||
let posX = 0;
|
let posX = 0;
|
||||||
|
@ -129,8 +59,6 @@ function getElementPosition(element: HTMLElement) {
|
||||||
y: posY,
|
y: posY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const points = ref<CaptchaPoint[]>([]);
|
|
||||||
const POINT_OFFSET = 11;
|
|
||||||
|
|
||||||
function handleClick(e: MouseEvent) {
|
function handleClick(e: MouseEvent) {
|
||||||
try {
|
try {
|
||||||
|
@ -151,15 +79,16 @@ function handleClick(e: MouseEvent) {
|
||||||
const x = Math.ceil(xPos);
|
const x = Math.ceil(xPos);
|
||||||
const y = Math.ceil(yPos);
|
const y = Math.ceil(yPos);
|
||||||
|
|
||||||
points.value.push({
|
const point = {
|
||||||
i: points.value.length,
|
i: points.value.length,
|
||||||
t: Date.now(),
|
t: Date.now(),
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
});
|
};
|
||||||
|
points.value.push(point);
|
||||||
|
|
||||||
emit('click', x, y);
|
emit('click', point);
|
||||||
e.cancelBubble = true;
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in handleClick:', error);
|
console.error('Error in handleClick:', error);
|
||||||
|
@ -184,6 +113,7 @@ function handleRefresh() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConfirm() {
|
function handleConfirm() {
|
||||||
|
if (!props.showConfirm) return;
|
||||||
try {
|
try {
|
||||||
emit('confirm', points.value, clear);
|
emit('confirm', points.value, clear);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -192,50 +122,64 @@ function handleConfirm() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Card :style="rootStyles" aria-labelledby="captcha-title" role="region">
|
<CaptchaCard
|
||||||
<CardHeader class="p-0">
|
:captcha-image="captchaImage"
|
||||||
<CardTitle id="captcha-title" class="flex items-center justify-between">
|
:height="height"
|
||||||
<span>{{ title }}</span>
|
:padding-x="paddingX"
|
||||||
<img
|
:padding-y="paddingY"
|
||||||
v-show="hintImage"
|
:title="title"
|
||||||
:src="hintImage"
|
:width="width"
|
||||||
:style="hintStyles"
|
@click="handleClick"
|
||||||
alt="提示图片"
|
>
|
||||||
/>
|
<template #title>
|
||||||
</CardTitle>
|
<slot name="title">{{ $t('captcha.title') }}</slot>
|
||||||
</CardHeader>
|
</template>
|
||||||
<CardContent class="relative mt-2 flex w-full overflow-hidden rounded p-0">
|
|
||||||
<img
|
<template #extra>
|
||||||
v-show="captchaImage"
|
<VbenIconButton
|
||||||
:src="captchaImage"
|
:aria-label="$t('captcha.refreshAriaLabel')"
|
||||||
:style="captchaStyles"
|
class="ml-1"
|
||||||
alt="验证码图片"
|
@click="handleRefresh"
|
||||||
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">
|
|
||||||
<RotateCw class="size-5" />
|
<RotateCw class="size-5" />
|
||||||
</VbenIconButton>
|
</VbenIconButton>
|
||||||
<VbenButton aria-label="确认选择" @click="handleConfirm">
|
<VbenButton
|
||||||
确认
|
v-if="showConfirm"
|
||||||
|
:aria-label="$t('captcha.confirmAriaLabel')"
|
||||||
|
class="ml-2"
|
||||||
|
size="sm"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>
|
||||||
|
{{ $t('captcha.confirm') }}
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
</CardFooter>
|
</template>
|
||||||
</Card>
|
|
||||||
|
<div
|
||||||
|
v-for="(point, index) in points"
|
||||||
|
:key="index"
|
||||||
|
:aria-label="$t('captcha.pointAriaLabel') + (index + 1)"
|
||||||
|
:style="{
|
||||||
|
top: `${point.y - POINT_OFFSET}px`,
|
||||||
|
left: `${point.x - POINT_OFFSET}px`,
|
||||||
|
}"
|
||||||
|
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>
|
||||||
|
<template #footer>
|
||||||
|
<img
|
||||||
|
v-if="hintImage"
|
||||||
|
:alt="$t('captcha.alt')"
|
||||||
|
:src="hintImage"
|
||||||
|
class="h-10 w-full rounded border border-solid border-slate-200"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="hintText"
|
||||||
|
class="flex h-10 w-full items-center justify-center rounded border border-solid border-slate-200"
|
||||||
|
>
|
||||||
|
{{ `${$t('captcha.clickInOrder')}` + `【${hintText}】` }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CaptchaCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,89 @@
|
||||||
export interface CaptchaPoint {
|
export interface CaptchaData {
|
||||||
i: number;
|
/**
|
||||||
|
* x
|
||||||
|
*/
|
||||||
x: number;
|
x: number;
|
||||||
|
/**
|
||||||
|
* y
|
||||||
|
*/
|
||||||
y: number;
|
y: number;
|
||||||
|
/**
|
||||||
|
* 时间戳
|
||||||
|
*/
|
||||||
t: number;
|
t: number;
|
||||||
}
|
}
|
||||||
|
export interface CaptchaPoint extends CaptchaData {
|
||||||
|
/**
|
||||||
|
* 数据索引
|
||||||
|
*/
|
||||||
|
i: number;
|
||||||
|
}
|
||||||
|
export interface CaptchaCardProps {
|
||||||
|
/**
|
||||||
|
* 验证码图片
|
||||||
|
*/
|
||||||
|
captchaImage: string;
|
||||||
|
/**
|
||||||
|
* 验证码图片高度
|
||||||
|
* @default '220px'
|
||||||
|
*/
|
||||||
|
height?: number | string;
|
||||||
|
/**
|
||||||
|
* 水平内边距
|
||||||
|
* @default '12px'
|
||||||
|
*/
|
||||||
|
paddingX?: number | string;
|
||||||
|
/**
|
||||||
|
* 垂直内边距
|
||||||
|
* @default '16px'
|
||||||
|
*/
|
||||||
|
paddingY?: number | string;
|
||||||
|
/**
|
||||||
|
* 标题
|
||||||
|
* @default '请按图依次点击'
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* 验证码图片宽度
|
||||||
|
* @default '300px'
|
||||||
|
*/
|
||||||
|
width?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PointSelectionCaptchaProps extends CaptchaCardProps {
|
||||||
|
/**
|
||||||
|
* 是否展示确定按钮
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
showConfirm?: boolean;
|
||||||
|
/**
|
||||||
|
* 提示图片
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
hintImage?: string;
|
||||||
|
/**
|
||||||
|
* 提示文本
|
||||||
|
* @default ''
|
||||||
|
*/
|
||||||
|
hintText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: 滑动验证码
|
||||||
|
*/
|
||||||
|
// export interface SlideCaptchaProps extends CaptchaCardProps {
|
||||||
|
// /**
|
||||||
|
// * 瓦片图片高度
|
||||||
|
// * @default '40px'
|
||||||
|
// */
|
||||||
|
// tileHeight?: number | string;
|
||||||
|
// /**
|
||||||
|
// * 瓦片图片宽度
|
||||||
|
// * @default '150px'
|
||||||
|
// */
|
||||||
|
// tileWidth?: number | string;
|
||||||
|
// /**
|
||||||
|
// * 瓦片图片
|
||||||
|
// */
|
||||||
|
// tileImage: string;
|
||||||
|
// }
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const parseValue = (value: number | string) => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const parsed = Number.parseFloat(value);
|
||||||
|
return Number.isNaN(parsed) ? 0 : parsed;
|
||||||
|
};
|
|
@ -311,5 +311,14 @@
|
||||||
"sidebarToggle": "Enable Sidebar Toggle",
|
"sidebarToggle": "Enable Sidebar Toggle",
|
||||||
"lockScreen": "Enable Lock Screen"
|
"lockScreen": "Enable Lock Screen"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"captcha": {
|
||||||
|
"alt": "Supports img tag src attribute value",
|
||||||
|
"title": "Please complete the security verification",
|
||||||
|
"refreshAriaLabel": "Refresh captcha",
|
||||||
|
"confirmAriaLabel": "Confirm selection",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"pointAriaLabel": "Click point",
|
||||||
|
"clickInOrder": "Please click in order"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -311,5 +311,14 @@
|
||||||
"sidebarToggle": "启用侧边栏切换",
|
"sidebarToggle": "启用侧边栏切换",
|
||||||
"lockScreen": "启用锁屏"
|
"lockScreen": "启用锁屏"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"captcha": {
|
||||||
|
"alt": "支持img标签src属性值",
|
||||||
|
"title": "请完成安全验证",
|
||||||
|
"refreshAriaLabel": "刷新验证码",
|
||||||
|
"confirmAriaLabel": "确认选择",
|
||||||
|
"confirm": "确认",
|
||||||
|
"pointAriaLabel": "点击点",
|
||||||
|
"clickInOrder": "请依次点击"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,27 @@
|
||||||
"custom": "Custom Component"
|
"custom": "Custom Component"
|
||||||
},
|
},
|
||||||
"captcha": {
|
"captcha": {
|
||||||
"title": "Captcha"
|
"title": "Captcha",
|
||||||
|
"captchaCardTitle": "Please complete the security verification",
|
||||||
|
"pageDescription": "Verify user identity by clicking on specific locations in the image.",
|
||||||
|
"pageTitle": "Captcha Component Example",
|
||||||
|
"basic": "Basic Usage",
|
||||||
|
"titlePlaceholder": "Captcha Title Text",
|
||||||
|
"captchaImageUrlPlaceholder": "Captcha Image (supports img tag src attribute value)",
|
||||||
|
"hintImage": "Hint Image",
|
||||||
|
"hintText": "Hint Text",
|
||||||
|
"hintImagePlaceholder": "Hint Image (supports img tag src attribute value)",
|
||||||
|
"hintTextPlaceholder": "Hint Text",
|
||||||
|
"showConfirm": "Show Confirm",
|
||||||
|
"hideConfirm": "Hide Confirm",
|
||||||
|
"widthPlaceholder": "Captcha Image Width Default 300px",
|
||||||
|
"heightPlaceholder": "Captcha Image Height Default 220px",
|
||||||
|
"paddingXPlaceholder": "Horizontal Padding Default 12px",
|
||||||
|
"paddingYPlaceholder": "Vertical Padding Default 16px",
|
||||||
|
"index": "Index:",
|
||||||
|
"timestamp": "Timestamp:",
|
||||||
|
"x": "x:",
|
||||||
|
"y": "y:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,27 @@
|
||||||
"custom": "自定义组件"
|
"custom": "自定义组件"
|
||||||
},
|
},
|
||||||
"captcha": {
|
"captcha": {
|
||||||
"title": "验证码"
|
"title": "验证码",
|
||||||
|
"captchaCardTitle": "请完成安全验证",
|
||||||
|
"pageDescription": "通过点击图片中的特定位置来验证用户身份。",
|
||||||
|
"pageTitle": "验证码组件示例",
|
||||||
|
"basic": "基本使用",
|
||||||
|
"titlePlaceholder": "验证码标题文案",
|
||||||
|
"captchaImageUrlPlaceholder": "验证码图片(支持img标签src属性值)",
|
||||||
|
"hintImage": "提示图片",
|
||||||
|
"hintText": "提示文本",
|
||||||
|
"hintImagePlaceholder": "提示图片(支持img标签src属性值)",
|
||||||
|
"hintTextPlaceholder": "提示文本",
|
||||||
|
"showConfirm": "展示确认",
|
||||||
|
"hideConfirm": "隐藏确认",
|
||||||
|
"widthPlaceholder": "验证码图片宽度 默认300px",
|
||||||
|
"heightPlaceholder": "验证码图片高度 默认220px",
|
||||||
|
"paddingXPlaceholder": "水平内边距 默认12px",
|
||||||
|
"paddingYPlaceholder": "垂直内边距 默认16px",
|
||||||
|
"index": "索引:",
|
||||||
|
"timestamp": "时间戳:",
|
||||||
|
"x": "x:",
|
||||||
|
"y": "y:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,177 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CaptchaPoint } from '@vben/common-ui';
|
import type { CaptchaPoint } from '@vben/common-ui';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { Page, PointSelectionCaptcha } from '@vben/common-ui';
|
import { Page, PointSelectionCaptcha } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Card } from 'ant-design-vue';
|
import { Card, Input, InputNumber, message, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { captchaImage, hintImage } from './base64';
|
import { captchaImage, hintImage } from './base64';
|
||||||
|
|
||||||
const selectedPoints = ref<CaptchaPoint[]>([]);
|
const selectedPoints = ref<CaptchaPoint[]>([]);
|
||||||
|
const params = reactive({
|
||||||
|
captchaImage,
|
||||||
|
captchaImageUrl: '',
|
||||||
|
height: undefined,
|
||||||
|
hintImage,
|
||||||
|
hintImageUrl: '',
|
||||||
|
hintText: '唇,燕,碴,找',
|
||||||
|
paddingX: undefined,
|
||||||
|
paddingY: undefined,
|
||||||
|
showConfirm: true,
|
||||||
|
showHintImage: true,
|
||||||
|
title: '',
|
||||||
|
width: undefined,
|
||||||
|
});
|
||||||
const handleConfirm = (points: CaptchaPoint[], clear: () => void) => {
|
const handleConfirm = (points: CaptchaPoint[], clear: () => void) => {
|
||||||
selectedPoints.value = points;
|
message.success({
|
||||||
|
content: `captcha points: ${JSON.stringify(points)}`,
|
||||||
|
});
|
||||||
clear();
|
clear();
|
||||||
|
selectedPoints.value = [];
|
||||||
};
|
};
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
selectedPoints.value = [];
|
selectedPoints.value = [];
|
||||||
};
|
};
|
||||||
|
const handleClick = (point: CaptchaPoint) => {
|
||||||
|
selectedPoints.value.push(point);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page
|
<Page
|
||||||
description="通过点击图片中的特定位置来验证用户身份。"
|
:description="$t('page.examples.captcha.pageDescription')"
|
||||||
title="验证码组件示例"
|
:title="$t('page.examples.captcha.pageTitle')"
|
||||||
>
|
>
|
||||||
<Card class="mb-4" title="基本使用">
|
<Card :title="$t('page.examples.captcha.basic')" class="mb-4">
|
||||||
<PointSelectionCaptcha
|
<div class="mb-3 flex items-center justify-start">
|
||||||
:captcha-image="captchaImage"
|
<Input
|
||||||
:hint-image="hintImage"
|
v-model:value="params.title"
|
||||||
class="float-left"
|
:placeholder="$t('page.examples.captcha.titlePlaceholder')"
|
||||||
@confirm="handleConfirm"
|
class="w-64"
|
||||||
@refresh="handleRefresh"
|
/>
|
||||||
/>
|
<Input
|
||||||
<div class="float-left p-5">
|
v-model:value="params.captchaImageUrl"
|
||||||
<div v-for="point in selectedPoints" :key="point.i" class="flex">
|
:placeholder="$t('page.examples.captcha.captchaImageUrlPlaceholder')"
|
||||||
<span class="mr-3 w-16">索引:{{ point.i }}</span>
|
class="ml-8 w-64"
|
||||||
<span class="mr-3 w-44">时间戳:{{ point.t }}</span>
|
/>
|
||||||
<span class="mr-3 w-16">x:{{ point.x }}</span>
|
<div class="ml-8 flex w-96 items-center">
|
||||||
<span class="mr-3 w-16">y:{{ point.y }}</span>
|
<Switch
|
||||||
|
v-model:checked="params.showHintImage"
|
||||||
|
:checked-children="$t('page.examples.captcha.hintImage')"
|
||||||
|
:un-checked-children="$t('page.examples.captcha.hintText')"
|
||||||
|
class="mr-4 w-40"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-show="params.showHintImage"
|
||||||
|
v-model:value="params.hintImageUrl"
|
||||||
|
:placeholder="$t('page.examples.captcha.hintImagePlaceholder')"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-show="!params.showHintImage"
|
||||||
|
v-model:value="params.hintText"
|
||||||
|
:placeholder="$t('page.examples.captcha.hintTextPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
v-model:checked="params.showConfirm"
|
||||||
|
:checked-children="$t('page.examples.captcha.showConfirm')"
|
||||||
|
:un-checked-children="$t('page.examples.captcha.hideConfirm')"
|
||||||
|
class="ml-8 w-28"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 flex items-center justify-start">
|
||||||
|
<div>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="params.width"
|
||||||
|
:min="1"
|
||||||
|
:placeholder="$t('page.examples.captcha.widthPlaceholder')"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
class="w-64"
|
||||||
|
>
|
||||||
|
<template #addonAfter>px</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
<div class="ml-8">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="params.height"
|
||||||
|
:min="1"
|
||||||
|
:placeholder="$t('page.examples.captcha.heightPlaceholder')"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
class="w-64"
|
||||||
|
>
|
||||||
|
<template #addonAfter>px</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
<div class="ml-8">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="params.paddingX"
|
||||||
|
:min="1"
|
||||||
|
:placeholder="$t('page.examples.captcha.paddingXPlaceholder')"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
class="w-64"
|
||||||
|
>
|
||||||
|
<template #addonAfter>px</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
<div class="ml-8">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="params.paddingY"
|
||||||
|
:min="1"
|
||||||
|
:placeholder="$t('page.examples.captcha.paddingYPlaceholder')"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
class="w-64"
|
||||||
|
>
|
||||||
|
<template #addonAfter>px</template>
|
||||||
|
</InputNumber>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PointSelectionCaptcha
|
||||||
|
:captcha-image="params.captchaImageUrl || params.captchaImage"
|
||||||
|
:height="params.height || 220"
|
||||||
|
:hint-image="
|
||||||
|
params.showHintImage ? params.hintImageUrl || params.hintImage : ''
|
||||||
|
"
|
||||||
|
:hint-text="params.hintText"
|
||||||
|
:padding-x="params.paddingX"
|
||||||
|
:padding-y="params.paddingY"
|
||||||
|
:show-confirm="params.showConfirm"
|
||||||
|
:width="params.width || 300"
|
||||||
|
class="float-left"
|
||||||
|
@click="handleClick"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
@refresh="handleRefresh"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ params.title || $t('page.examples.captcha.captchaCardTitle') }}
|
||||||
|
</template>
|
||||||
|
</PointSelectionCaptcha>
|
||||||
|
|
||||||
|
<ol class="float-left p-5">
|
||||||
|
<li v-for="point in selectedPoints" :key="point.i" class="flex">
|
||||||
|
<span class="mr-3 w-16">{{
|
||||||
|
$t('page.examples.captcha.index') + point.i
|
||||||
|
}}</span>
|
||||||
|
<span class="mr-3 w-52">{{
|
||||||
|
$t('page.examples.captcha.timestamp') + point.t
|
||||||
|
}}</span>
|
||||||
|
<span class="mr-3 w-16">{{
|
||||||
|
$t('page.examples.captcha.x') + point.x
|
||||||
|
}}</span>
|
||||||
|
<span class="mr-3 w-16">{{
|
||||||
|
$t('page.examples.captcha.y') + point.y
|
||||||
|
}}</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
</Card>
|
</Card>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue