committed by
GitHub
132 changed files with 6308 additions and 4058 deletions
@ -0,0 +1,28 @@ |
|||
export default eventHandler(async (event) => { |
|||
const userinfo = verifyAccessToken(event); |
|||
if (!userinfo) { |
|||
return unAuthorizedResponse(event); |
|||
} |
|||
const data = ` |
|||
{ |
|||
"code": 0, |
|||
"message": "success", |
|||
"data": [ |
|||
{ |
|||
"id": 123456789012345678901234567890123456789012345678901234567890, |
|||
"name": "John Doe", |
|||
"age": 30, |
|||
"email": "john-doe@demo.com" |
|||
}, |
|||
{ |
|||
"id": 987654321098765432109876543210987654321098765432109876543210, |
|||
"name": "Jane Smith", |
|||
"age": 25, |
|||
"email": "jane@demo.com" |
|||
} |
|||
] |
|||
} |
|||
`;
|
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return data; |
|||
}); |
|||
@ -0,0 +1,71 @@ |
|||
<script lang="ts" setup> |
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
|
|||
import { useVbenForm } from '#/adapter/form'; |
|||
|
|||
defineOptions({ |
|||
name: 'FormModelDemo', |
|||
}); |
|||
|
|||
const [Form, formApi] = useVbenForm({ |
|||
schema: [ |
|||
{ |
|||
component: 'Input', |
|||
componentProps: { |
|||
placeholder: '请输入', |
|||
}, |
|||
fieldName: 'field1', |
|||
label: '字段1', |
|||
rules: 'required', |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
componentProps: { |
|||
placeholder: '请输入', |
|||
}, |
|||
fieldName: 'field2', |
|||
label: '字段2', |
|||
rules: 'required', |
|||
}, |
|||
{ |
|||
component: 'Select', |
|||
componentProps: { |
|||
options: [ |
|||
{ label: '选项1', value: '1' }, |
|||
{ label: '选项2', value: '2' }, |
|||
], |
|||
placeholder: '请输入', |
|||
}, |
|||
fieldName: 'field3', |
|||
label: '字段3', |
|||
rules: 'required', |
|||
}, |
|||
], |
|||
showDefaultActions: false, |
|||
}); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
fullscreenButton: false, |
|||
onCancel() { |
|||
modalApi.close(); |
|||
}, |
|||
onConfirm: async () => { |
|||
await formApi.validateAndSubmitForm(); |
|||
// modalApi.close(); |
|||
}, |
|||
onOpenChange(isOpen: boolean) { |
|||
if (isOpen) { |
|||
const { values } = modalApi.getData<Record<string, any>>(); |
|||
if (values) { |
|||
formApi.setValues(values); |
|||
} |
|||
} |
|||
}, |
|||
title: '内嵌表单示例', |
|||
}); |
|||
</script> |
|||
<template> |
|||
<Modal> |
|||
<Form /> |
|||
</Modal> |
|||
</template> |
|||
@ -0,0 +1,16 @@ |
|||
<script lang="ts" setup> |
|||
import { EllipsisText } from '@vben/common-ui'; |
|||
|
|||
const text = ` |
|||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。 |
|||
`; |
|||
</script> |
|||
<template> |
|||
<EllipsisText :line="2" :tooltip-when-ellipsis="true"> |
|||
{{ text }} |
|||
</EllipsisText> |
|||
|
|||
<EllipsisText :line="3" :tooltip-when-ellipsis="true"> |
|||
{{ text }} |
|||
</EllipsisText> |
|||
</template> |
|||
@ -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> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue