Browse Source
refactor: refactor code structure and improve demo page (#4389)
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/4404/head
Squall2017
6 days ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 459 additions and 157 deletions
-
77packages/effects/common-ui/src/components/captcha/captcha-card.vue
-
1packages/effects/common-ui/src/components/captcha/index.ts
-
216packages/effects/common-ui/src/components/captcha/point-selection-captcha.vue
-
87packages/effects/common-ui/src/components/captcha/types.ts
-
7packages/effects/common-ui/src/components/captcha/utils.ts
-
9packages/locales/src/langs/en-US.json
-
9packages/locales/src/langs/zh-CN.json
-
22playground/src/locales/langs/en-US.json
-
22playground/src/locales/langs/zh-CN.json
-
166playground/src/views/examples/captcha/index.vue
@ -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 type * from './types'; |
@ -1,6 +1,89 @@ |
|||
export interface CaptchaPoint { |
|||
i: number; |
|||
export interface CaptchaData { |
|||
/** |
|||
* x |
|||
*/ |
|||
x: number; |
|||
/** |
|||
* y |
|||
*/ |
|||
y: 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; |
|||
}; |
@ -1,45 +1,177 @@ |
|||
<script lang="ts" setup> |
|||
import type { CaptchaPoint } from '@vben/common-ui'; |
|||
|
|||
import { ref } from 'vue'; |
|||
import { reactive, ref } from 'vue'; |
|||
|
|||
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'; |
|||
|
|||
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) => { |
|||
selectedPoints.value = points; |
|||
message.success({ |
|||
content: `captcha points: ${JSON.stringify(points)}`, |
|||
}); |
|||
clear(); |
|||
selectedPoints.value = []; |
|||
}; |
|||
const handleRefresh = () => { |
|||
selectedPoints.value = []; |
|||
}; |
|||
const handleClick = (point: CaptchaPoint) => { |
|||
selectedPoints.value.push(point); |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<Page |
|||
description="通过点击图片中的特定位置来验证用户身份。" |
|||
title="验证码组件示例" |
|||
:description="$t('page.examples.captcha.pageDescription')" |
|||
:title="$t('page.examples.captcha.pageTitle')" |
|||
> |
|||
<Card class="mb-4" title="基本使用"> |
|||
<Card :title="$t('page.examples.captcha.basic')" class="mb-4"> |
|||
<div class="mb-3 flex items-center justify-start"> |
|||
<Input |
|||
v-model:value="params.title" |
|||
:placeholder="$t('page.examples.captcha.titlePlaceholder')" |
|||
class="w-64" |
|||
/> |
|||
<Input |
|||
v-model:value="params.captchaImageUrl" |
|||
:placeholder="$t('page.examples.captcha.captchaImageUrlPlaceholder')" |
|||
class="ml-8 w-64" |
|||
/> |
|||
<div class="ml-8 flex w-96 items-center"> |
|||
<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> |
|||
|
|||
<PointSelectionCaptcha |
|||
:captcha-image="captchaImage" |
|||
:hint-image="hintImage" |
|||
: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" |
|||
/> |
|||
<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> |
|||
> |
|||
<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> |
|||
</Page> |
|||
</template> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue