Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -48,13 +48,18 @@ const queryFormStyle = computed(() => {
|
||||
async function handleSubmit(e: Event) {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
const { valid } = await form.validate();
|
||||
const props = unref(rootProps);
|
||||
if (!props.formApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { valid } = await props.formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = toRaw(await unref(rootProps).formApi?.getValues());
|
||||
await unref(rootProps).handleSubmit?.(values);
|
||||
const values = toRaw(await props.formApi.getValues());
|
||||
await props.handleSubmit?.(values);
|
||||
}
|
||||
|
||||
async function handleReset(e: Event) {
|
||||
|
||||
@@ -39,6 +39,7 @@ function getDefaultState(): VbenFormProps {
|
||||
layout: 'horizontal',
|
||||
resetButtonOptions: {},
|
||||
schema: [],
|
||||
scrollToFirstError: false,
|
||||
showCollapseButton: false,
|
||||
showDefaultActions: true,
|
||||
submitButtonOptions: {},
|
||||
@@ -253,6 +254,41 @@ export class FormApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到第一个错误字段
|
||||
* @param errors 验证错误对象
|
||||
*/
|
||||
scrollToFirstError(errors: Record<string, any> | string) {
|
||||
// https://github.com/logaretm/vee-validate/discussions/3835
|
||||
const firstErrorFieldName =
|
||||
typeof errors === 'string' ? errors : Object.keys(errors)[0];
|
||||
|
||||
if (!firstErrorFieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = document.querySelector(
|
||||
`[name="${firstErrorFieldName}"]`,
|
||||
) as HTMLElement;
|
||||
|
||||
// 如果通过 name 属性找不到,尝试通过组件引用查找, 正常情况下不会走到这,怕哪天 vee-validate 改了 name 属性有个兜底的
|
||||
if (!el) {
|
||||
const componentRef = this.getFieldComponentRef(firstErrorFieldName);
|
||||
if (componentRef && componentRef.$el instanceof HTMLElement) {
|
||||
el = componentRef.$el;
|
||||
}
|
||||
}
|
||||
|
||||
if (el) {
|
||||
// 滚动到错误字段,添加一些偏移量以确保字段完全可见
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
|
||||
const form = await this.getForm();
|
||||
form.setFieldValue(field, value, shouldValidate);
|
||||
@@ -389,14 +425,21 @@ export class FormApi {
|
||||
|
||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||
console.error('validate error', validateResult?.errors);
|
||||
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(validateResult.errors);
|
||||
}
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
async validateAndSubmitForm() {
|
||||
const form = await this.getForm();
|
||||
const { valid } = await form.validate();
|
||||
const { valid, errors } = await form.validate();
|
||||
if (!valid) {
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(errors);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return await this.submitForm();
|
||||
@@ -408,6 +451,10 @@ export class FormApi {
|
||||
|
||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||
console.error('validate error', validateResult?.errors);
|
||||
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(fieldName);
|
||||
}
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
@@ -389,6 +389,12 @@ export interface VbenFormProps<
|
||||
*/
|
||||
resetButtonOptions?: ActionButtonOptions;
|
||||
|
||||
/**
|
||||
* 验证失败时是否自动滚动到第一个错误字段
|
||||
* @default false
|
||||
*/
|
||||
scrollToFirstError?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示默认操作按钮
|
||||
* @default true
|
||||
|
||||
@@ -287,7 +287,11 @@ defineExpose({
|
||||
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
|
||||
>
|
||||
<ChevronRight
|
||||
v-if="item.hasChildren"
|
||||
v-if="
|
||||
item.hasChildren &&
|
||||
Array.isArray(item.value[childrenField]) &&
|
||||
item.value[childrenField].length > 0
|
||||
"
|
||||
class="size-4 cursor-pointer transition"
|
||||
:class="{ 'rotate-90': isExpanded }"
|
||||
@click.stop="
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@vben/types": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"@vueuse/integrations": "catalog:",
|
||||
"json-bigint": "catalog:",
|
||||
"crypto-js": "catalog:",
|
||||
"qrcode": "catalog:",
|
||||
"tippy.js": "catalog:",
|
||||
|
||||
@@ -3,6 +3,7 @@ export { default as PointSelectionCaptchaCard } from './point-selection-captcha/
|
||||
|
||||
export { default as SliderCaptcha } from './slider-captcha/index.vue';
|
||||
export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue';
|
||||
export { default as SliderTranslateCaptcha } from './slider-translate-captcha/index.vue';
|
||||
export type * from './types';
|
||||
|
||||
export { default as Verification } from './verification/index.vue';
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
CaptchaVerifyPassingData,
|
||||
SliderCaptchaActionType,
|
||||
SliderRotateVerifyPassingData,
|
||||
SliderTranslateCaptchaProps,
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
unref,
|
||||
useTemplateRef,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SliderCaptcha from '../slider-captcha/index.vue';
|
||||
|
||||
const props = withDefaults(defineProps<SliderTranslateCaptchaProps>(), {
|
||||
defaultTip: '',
|
||||
canvasWidth: 420,
|
||||
canvasHeight: 280,
|
||||
squareLength: 42,
|
||||
circleRadius: 10,
|
||||
src: '',
|
||||
diffDistance: 3,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [CaptchaVerifyPassingData];
|
||||
}>();
|
||||
|
||||
const PI: number = Math.PI;
|
||||
enum CanvasOpr {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Clip = 'clip',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Fill = 'fill',
|
||||
}
|
||||
|
||||
const modalValue = defineModel<boolean>({ default: false });
|
||||
|
||||
const slideBarRef = useTemplateRef<SliderCaptchaActionType>('slideBarRef');
|
||||
const puzzleCanvasRef = useTemplateRef<HTMLCanvasElement>('puzzleCanvasRef');
|
||||
const pieceCanvasRef = useTemplateRef<HTMLCanvasElement>('pieceCanvasRef');
|
||||
|
||||
const state = reactive({
|
||||
dragging: false,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
pieceX: 0,
|
||||
pieceY: 0,
|
||||
moveDistance: 0,
|
||||
isPassing: false,
|
||||
showTip: false,
|
||||
});
|
||||
|
||||
const left = ref('0');
|
||||
|
||||
const pieceStyle = computed(() => {
|
||||
return {
|
||||
left: left.value,
|
||||
};
|
||||
});
|
||||
|
||||
function setLeft(val: string) {
|
||||
left.value = val;
|
||||
}
|
||||
|
||||
const verifyTip = computed(() => {
|
||||
return state.isPassing
|
||||
? $t('ui.captcha.sliderTranslateSuccessTip', [
|
||||
((state.endTime - state.startTime) / 1000).toFixed(1),
|
||||
])
|
||||
: $t('ui.captcha.sliderTranslateFailTip');
|
||||
});
|
||||
function handleStart() {
|
||||
state.startTime = Date.now();
|
||||
}
|
||||
|
||||
function handleDragBarMove(data: SliderRotateVerifyPassingData) {
|
||||
state.dragging = true;
|
||||
const { moveX } = data;
|
||||
state.moveDistance = moveX;
|
||||
setLeft(`${moveX}px`);
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
const { pieceX } = state;
|
||||
const { diffDistance } = props;
|
||||
|
||||
if (Math.abs(pieceX - state.moveDistance) >= (diffDistance || 3)) {
|
||||
setLeft('0');
|
||||
state.moveDistance = 0;
|
||||
} else {
|
||||
checkPass();
|
||||
}
|
||||
state.showTip = true;
|
||||
state.dragging = false;
|
||||
}
|
||||
|
||||
function checkPass() {
|
||||
state.isPassing = true;
|
||||
state.endTime = Date.now();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => state.isPassing,
|
||||
(isPassing) => {
|
||||
if (isPassing) {
|
||||
const { endTime, startTime } = state;
|
||||
const time = (endTime - startTime) / 1000;
|
||||
emit('success', { isPassing, time: time.toFixed(1) });
|
||||
}
|
||||
modalValue.value = isPassing;
|
||||
},
|
||||
);
|
||||
|
||||
function resetCanvas() {
|
||||
const { canvasWidth, canvasHeight } = props;
|
||||
const puzzleCanvas = unref(puzzleCanvasRef);
|
||||
const pieceCanvas = unref(pieceCanvasRef);
|
||||
if (!puzzleCanvas || !pieceCanvas) return;
|
||||
pieceCanvas.width = canvasWidth;
|
||||
const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
|
||||
// Canvas2D: Multiple readback operations using getImageData
|
||||
// are faster with the willReadFrequently attribute set to true.
|
||||
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
|
||||
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
|
||||
willReadFrequently: true,
|
||||
});
|
||||
if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
|
||||
puzzleCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
pieceCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const { canvasWidth, canvasHeight, squareLength, circleRadius, src } = props;
|
||||
const puzzleCanvas = unref(puzzleCanvasRef);
|
||||
const pieceCanvas = unref(pieceCanvasRef);
|
||||
if (!puzzleCanvas || !pieceCanvas) return;
|
||||
const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
|
||||
// Canvas2D: Multiple readback operations using getImageData
|
||||
// are faster with the willReadFrequently attribute set to true.
|
||||
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
|
||||
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
|
||||
willReadFrequently: true,
|
||||
});
|
||||
if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
|
||||
const img = new Image();
|
||||
// 解决跨域
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = src;
|
||||
img.addEventListener('load', () => {
|
||||
draw(puzzleCanvasCtx, pieceCanvasCtx);
|
||||
puzzleCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||
pieceCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||
const pieceLength = squareLength + 2 * circleRadius + 3;
|
||||
const sx = state.pieceX;
|
||||
const sy = state.pieceY - 2 * circleRadius - 1;
|
||||
const imageData = pieceCanvasCtx.getImageData(
|
||||
sx,
|
||||
sy,
|
||||
pieceLength,
|
||||
pieceLength,
|
||||
);
|
||||
pieceCanvas.width = pieceLength;
|
||||
pieceCanvasCtx.putImageData(imageData, 0, sy);
|
||||
setLeft('0');
|
||||
});
|
||||
}
|
||||
|
||||
function getRandomNumberByRange(start: number, end: number) {
|
||||
return Math.round(Math.random() * (end - start) + start);
|
||||
}
|
||||
|
||||
// 绘制拼图
|
||||
function draw(ctx1: CanvasRenderingContext2D, ctx2: CanvasRenderingContext2D) {
|
||||
const { canvasWidth, canvasHeight, squareLength, circleRadius } = props;
|
||||
state.pieceX = getRandomNumberByRange(
|
||||
squareLength + 2 * circleRadius,
|
||||
canvasWidth - (squareLength + 2 * circleRadius),
|
||||
);
|
||||
state.pieceY = getRandomNumberByRange(
|
||||
3 * circleRadius,
|
||||
canvasHeight - (squareLength + 2 * circleRadius),
|
||||
);
|
||||
drawPiece(ctx1, state.pieceX, state.pieceY, CanvasOpr.Fill);
|
||||
drawPiece(ctx2, state.pieceX, state.pieceY, CanvasOpr.Clip);
|
||||
}
|
||||
|
||||
// 绘制拼图切块
|
||||
function drawPiece(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
opr: CanvasOpr,
|
||||
) {
|
||||
const { squareLength, circleRadius } = props;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.arc(
|
||||
x + squareLength / 2,
|
||||
y - circleRadius + 2,
|
||||
circleRadius,
|
||||
0.72 * PI,
|
||||
2.26 * PI,
|
||||
);
|
||||
ctx.lineTo(x + squareLength, y);
|
||||
ctx.arc(
|
||||
x + squareLength + circleRadius - 2,
|
||||
y + squareLength / 2,
|
||||
circleRadius,
|
||||
1.21 * PI,
|
||||
2.78 * PI,
|
||||
);
|
||||
ctx.lineTo(x + squareLength, y + squareLength);
|
||||
ctx.lineTo(x, y + squareLength);
|
||||
ctx.arc(
|
||||
x + circleRadius - 2,
|
||||
y + squareLength / 2,
|
||||
circleRadius + 0.4,
|
||||
2.76 * PI,
|
||||
1.24 * PI,
|
||||
true,
|
||||
);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
|
||||
ctx.stroke();
|
||||
opr === CanvasOpr.Clip ? ctx.clip() : ctx.fill();
|
||||
ctx.globalCompositeOperation = 'destination-over';
|
||||
}
|
||||
|
||||
function resume() {
|
||||
state.showTip = false;
|
||||
const basicEl = unref(slideBarRef);
|
||||
if (!basicEl) {
|
||||
return;
|
||||
}
|
||||
state.dragging = false;
|
||||
state.isPassing = false;
|
||||
state.pieceX = 0;
|
||||
state.pieceY = 0;
|
||||
|
||||
basicEl.resume();
|
||||
resetCanvas();
|
||||
initCanvas();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col items-center">
|
||||
<div
|
||||
class="border-border relative flex cursor-pointer overflow-hidden border shadow-md"
|
||||
>
|
||||
<canvas
|
||||
ref="puzzleCanvasRef"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
@click="resume"
|
||||
></canvas>
|
||||
<canvas
|
||||
ref="pieceCanvasRef"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
:style="pieceStyle"
|
||||
class="absolute"
|
||||
@click="resume"
|
||||
></canvas>
|
||||
<div
|
||||
class="h-15 absolute bottom-3 left-0 z-10 block w-full text-center text-xs leading-[30px] text-white"
|
||||
>
|
||||
<div
|
||||
v-if="state.showTip"
|
||||
:class="{
|
||||
'bg-success/80': state.isPassing,
|
||||
'bg-destructive/80': !state.isPassing,
|
||||
}"
|
||||
>
|
||||
{{ verifyTip }}
|
||||
</div>
|
||||
<div v-if="!state.dragging" class="bg-black/30">
|
||||
{{ defaultTip || $t('ui.captcha.sliderTranslateDefaultTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SliderCaptcha
|
||||
ref="slideBarRef"
|
||||
v-model="modalValue"
|
||||
class="mt-5"
|
||||
is-slot
|
||||
@end="handleDragEnd"
|
||||
@move="handleDragBarMove"
|
||||
@start="handleStart"
|
||||
>
|
||||
<template v-for="(_, key) in $slots" :key="key" #[key]="slotProps">
|
||||
<slot :name="key" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
</SliderCaptcha>
|
||||
</div>
|
||||
</template>
|
||||
@@ -159,6 +159,42 @@ export interface SliderRotateCaptchaProps {
|
||||
defaultTip?: string;
|
||||
}
|
||||
|
||||
export interface SliderTranslateCaptchaProps {
|
||||
/**
|
||||
* @description 拼图的宽度
|
||||
* @default 420
|
||||
*/
|
||||
canvasWidth?: number;
|
||||
/**
|
||||
* @description 拼图的高度
|
||||
* @default 280
|
||||
*/
|
||||
canvasHeight?: number;
|
||||
/**
|
||||
* @description 切块上正方形的长度
|
||||
* @default 42
|
||||
*/
|
||||
squareLength?: number;
|
||||
/**
|
||||
* @description 切块上圆形的半径
|
||||
* @default 10
|
||||
*/
|
||||
circleRadius?: number;
|
||||
/**
|
||||
* @description 图片的地址
|
||||
*/
|
||||
src?: string;
|
||||
/**
|
||||
* @description 允许的最大差距
|
||||
* @default 3
|
||||
*/
|
||||
diffDistance?: number;
|
||||
/**
|
||||
* @description 默认提示文本
|
||||
*/
|
||||
defaultTip?: string;
|
||||
}
|
||||
|
||||
export interface CaptchaVerifyPassingData {
|
||||
isPassing: boolean;
|
||||
time: number | string;
|
||||
|
||||
@@ -81,7 +81,7 @@ watch(keywordDebounce, () => {
|
||||
currentPage.value = 1;
|
||||
setCurrentPage(1);
|
||||
});
|
||||
|
||||
|
||||
watchDebounced(
|
||||
() => props.prefix,
|
||||
async (prefix) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
VbenAvatar,
|
||||
VbenButton,
|
||||
VbenButtonGroup,
|
||||
VbenCheckbox,
|
||||
VbenCheckButtonGroup,
|
||||
VbenCountToAnimator,
|
||||
VbenFullScreen,
|
||||
@@ -25,6 +26,7 @@ export {
|
||||
VbenLoading,
|
||||
VbenLogo,
|
||||
VbenPinInput,
|
||||
VbenSelect,
|
||||
VbenSpinner,
|
||||
VbenTree,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
@@ -18,6 +18,9 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import { isBoolean } from '@vben-core/shared/utils';
|
||||
|
||||
// @ts-ignore
|
||||
import JsonBigint from 'json-bigint';
|
||||
|
||||
defineOptions({ name: 'JsonViewer' });
|
||||
|
||||
const props = withDefaults(defineProps<JsonViewerProps>(), {
|
||||
@@ -68,6 +71,20 @@ function handleClick(event: MouseEvent) {
|
||||
emit('click', event);
|
||||
}
|
||||
|
||||
// 支持显示 bigint 数据,如较长的订单号
|
||||
const jsonData = computed<Record<string, any>>(() => {
|
||||
if (typeof props.value !== 'string') {
|
||||
return props.value || {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JsonBigint({ storeAsString: true }).parse(props.value);
|
||||
} catch (error) {
|
||||
console.error('JSON parse error:', error);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const bindProps = computed<Recordable<any>>(() => {
|
||||
const copyable = {
|
||||
copyText: $t('ui.jsonViewer.copy'),
|
||||
@@ -79,6 +96,7 @@ const bindProps = computed<Recordable<any>>(() => {
|
||||
return {
|
||||
...props,
|
||||
...attrs,
|
||||
value: jsonData.value,
|
||||
onCopied: (event: JsonViewerAction) => emit('copied', event),
|
||||
onKeyclick: (key: string) => emit('keyClick', key),
|
||||
onClick: (event: MouseEvent) => handleClick(event),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type { VxeGridSlots, VxeGridSlotTypes } from 'vxe-table';
|
||||
|
||||
import type { SlotsType } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben-core/form-ui';
|
||||
|
||||
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
||||
@@ -9,6 +13,12 @@ import { useStore } from '@vben-core/shared/store';
|
||||
import { VxeGridApi } from './api';
|
||||
import VxeGrid from './use-vxe-grid.vue';
|
||||
|
||||
type FilteredSlots<T> = {
|
||||
[K in keyof VxeGridSlots<T> as K extends 'form'
|
||||
? never
|
||||
: K]: VxeGridSlots<T>[K];
|
||||
};
|
||||
|
||||
export function useVbenVxeGrid<
|
||||
T extends Record<string, any> = any,
|
||||
D extends BaseFormComponentType = BaseFormComponentType,
|
||||
@@ -31,6 +41,16 @@ export function useVbenVxeGrid<
|
||||
{
|
||||
name: 'VbenVxeGrid',
|
||||
inheritAttrs: false,
|
||||
slots: Object as SlotsType<
|
||||
{
|
||||
// 表格标题
|
||||
'table-title': undefined;
|
||||
// 工具栏左侧部分
|
||||
'toolbar-actions': VxeGridSlotTypes.DefaultSlotParams<T>;
|
||||
// 工具栏右侧部分
|
||||
'toolbar-tools': VxeGridSlotTypes.DefaultSlotParams<T>;
|
||||
} & FilteredSlots<T>
|
||||
>,
|
||||
},
|
||||
);
|
||||
// Add reactivity support
|
||||
|
||||
@@ -46,8 +46,11 @@
|
||||
"sliderDefaultText": "Slider and drag",
|
||||
"alt": "Supports img tag src attribute value",
|
||||
"sliderRotateDefaultTip": "Click picture to refresh",
|
||||
"sliderTranslateDefaultTip": "Click picture to refresh",
|
||||
"sliderRotateFailTip": "Validation failed",
|
||||
"sliderRotateSuccessTip": "Validation successful, time {0} seconds",
|
||||
"sliderTranslateFailTip": "Validation failed",
|
||||
"sliderTranslateSuccessTip": "Validation successful, time {0} seconds",
|
||||
"refreshAriaLabel": "Refresh captcha",
|
||||
"confirmAriaLabel": "Confirm selection",
|
||||
"confirm": "Confirm",
|
||||
|
||||
@@ -45,8 +45,11 @@
|
||||
"sliderSuccessText": "验证通过",
|
||||
"sliderDefaultText": "请按住滑块拖动",
|
||||
"sliderRotateDefaultTip": "点击图片可刷新",
|
||||
"sliderTranslateDefaultTip": "点击图片可刷新",
|
||||
"sliderRotateFailTip": "验证失败",
|
||||
"sliderRotateSuccessTip": "验证成功,耗时{0}秒",
|
||||
"sliderTranslateFailTip": "验证失败",
|
||||
"sliderTranslateSuccessTip": "验证成功,耗时{0}秒",
|
||||
"alt": "支持img标签src属性值",
|
||||
"refreshAriaLabel": "刷新验证码",
|
||||
"confirmAriaLabel": "确认选择",
|
||||
|
||||
Reference in New Issue
Block a user