forked from Mirrors/elk
110 lines
2.8 KiB
Vue
110 lines
2.8 KiB
Vue
|
<script lang="ts" setup>
|
||
|
import type { Boundaries } from 'vue-advanced-cropper'
|
||
|
import { Cropper } from 'vue-advanced-cropper'
|
||
|
import 'vue-advanced-cropper/dist/style.css'
|
||
|
|
||
|
export interface Props {
|
||
|
/** Images to be cropped */
|
||
|
modelValue?: File
|
||
|
/** Crop frame aspect ratio (width/height), default 1/1 */
|
||
|
stencilAspectRatio?: number
|
||
|
/** The ratio of the longest edge of the cut box to the length of the cut screen, default 0.9, not more than 1 */
|
||
|
stencilSizePercentage?: number
|
||
|
}
|
||
|
const props = withDefaults(defineProps<Props>(), {
|
||
|
stencilAspectRatio: 1 / 1,
|
||
|
stencilSizePercentage: 0.9,
|
||
|
})
|
||
|
|
||
|
const emits = defineEmits<{
|
||
|
(event: 'update:modelValue', value: File): void
|
||
|
}>()
|
||
|
|
||
|
const vmFile = useVModel(props, 'modelValue', emits, { passive: true })
|
||
|
|
||
|
const cropperDialog = ref(false)
|
||
|
|
||
|
const cropper = ref<InstanceType<typeof Cropper>>()
|
||
|
|
||
|
const cropperFlag = ref(false)
|
||
|
|
||
|
const cropperImage = reactive({
|
||
|
src: '',
|
||
|
type: 'image/jpg',
|
||
|
})
|
||
|
|
||
|
const stencilSize = ({ boundaries }: { boundaries: Boundaries }) => {
|
||
|
return {
|
||
|
width: boundaries.width * props.stencilSizePercentage,
|
||
|
height: boundaries.height * props.stencilSizePercentage,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
watch(vmFile, (file, _, onCleanup) => {
|
||
|
let expired = false
|
||
|
onCleanup(() => expired = true)
|
||
|
|
||
|
if (file && !cropperFlag.value) {
|
||
|
cropperDialog.value = true
|
||
|
const reader = new FileReader()
|
||
|
reader.readAsDataURL(file)
|
||
|
reader.onload = (e) => {
|
||
|
if (expired)
|
||
|
return
|
||
|
cropperImage.src = e.target?.result as string
|
||
|
cropperImage.type = file.type
|
||
|
}
|
||
|
}
|
||
|
cropperFlag.value = false
|
||
|
})
|
||
|
|
||
|
const cropImage = () => {
|
||
|
if (cropper.value && vmFile.value) {
|
||
|
cropperFlag.value = true
|
||
|
cropperDialog.value = false
|
||
|
const { canvas } = cropper.value.getResult()
|
||
|
canvas?.toBlob((blob) => {
|
||
|
vmFile.value = new File([blob as any], `cropped${vmFile.value?.name}` as string, { type: blob?.type })
|
||
|
}, cropperImage.type)
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<template>
|
||
|
<ModalDialog v-model="cropperDialog" :use-v-if="false" max-w-500px flex>
|
||
|
<div flex-1 w-0>
|
||
|
<div text-lg text-center my2 px3>
|
||
|
<h1>
|
||
|
{{ $t('action.edit') }}
|
||
|
</h1>
|
||
|
</div>
|
||
|
<div aspect-ratio-1>
|
||
|
<Cropper
|
||
|
ref="cropper"
|
||
|
class="overflow-hidden w-full h-full"
|
||
|
:src="cropperImage.src"
|
||
|
:resize-image="{
|
||
|
adjustStencil: false,
|
||
|
}"
|
||
|
:stencil-size="stencilSize"
|
||
|
:stencil-props="{
|
||
|
aspectRatio: props.stencilAspectRatio,
|
||
|
movable: false,
|
||
|
resizable: false,
|
||
|
handlers: {},
|
||
|
}"
|
||
|
image-restriction="stencil"
|
||
|
/>
|
||
|
</div>
|
||
|
<div m-4>
|
||
|
<button
|
||
|
btn-solid w-full rounded text-sm
|
||
|
@click="cropImage()"
|
||
|
>
|
||
|
{{ $t('action.confirm') }}
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
</ModalDialog>
|
||
|
</template>
|