| 1 | <template> |
| 2 | <div class="image-cropper"> |
| 3 | <slot :open-cropper /> |
| 4 | |
| 5 | <n-modal v-model:show="showCropper"> |
| 6 | <n-card class="image-cropper-modal flex flex-col" content-class="p-5!"> |
| 7 | <div class="aspect-square"> |
| 8 | <div v-if="!img" class="upload-box"> |
| 9 | <n-upload v-show="!img" accept="image/*" :show-file-list="false" @change="setImage"> |
| 10 | <n-upload-dragger> |
| 11 | <div>{{ placeholder }}</div> |
| 12 | </n-upload-dragger> |
| 13 | </n-upload> |
| 14 | </div> |
| 15 | <div v-if="img" class="crop-box"> |
| 16 | <Cropper |
| 17 | ref="cropper" |
| 18 | class="cropper aspect-square" |
| 19 | :src="img" |
| 20 | :stencil-size |
| 21 | :stencil-props |
| 22 | :resize-image |
| 23 | image-restriction="stencil" |
| 24 | :stencil-component="stencil" |
| 25 | /> |
| 26 | </div> |
| 27 | </div> |
| 28 | <div class="mt-4 flex justify-end gap-4"> |
| 29 | <n-button secondary @click="closeCropper()">Close</n-button> |
| 30 | <n-button v-if="img" type="primary" @click="emitCrop()">Save</n-button> |
| 31 | </div> |
| 32 | </n-card> |
| 33 | </n-modal> |
| 34 | </div> |
| 35 | </template> |
| 36 | |
| 37 | <script lang="ts" setup> |
| 38 | import type { UploadSettledFileInfo } from "naive-ui" |
| 39 | import type { CropperResult } from "vue-advanced-cropper" |
| 40 | import { NButton, NCard, NModal, NUpload, NUploadDragger } from "naive-ui" |
| 41 | import { computed, ref } from "vue" |
| 42 | import { CircleStencil, Cropper, RectangleStencil } from "vue-advanced-cropper" |
| 43 | import "vue-advanced-cropper/dist/style.css" |
| 44 | |
| 45 | export type ImageCropperResult = CropperResult |
| 46 | |
| 47 | const { placeholder = "Select an image", shape = "square" } = defineProps<{ |
| 48 | placeholder?: string |
| 49 | shape?: "square" | "circle" |
| 50 | }>() |
| 51 | |
| 52 | const emit = defineEmits<{ |
| 53 | (e: "crop", value: ImageCropperResult): void |
| 54 | }>() |
| 55 | |
| 56 | const stencil = computed(() => (shape === "circle" ? CircleStencil : RectangleStencil)) |
| 57 | const img = ref("") |
| 58 | const showCropper = ref(false) |
| 59 | const cropper = ref<typeof Cropper | null>(null) |
| 60 | |
| 61 | const stencilSize = ref({ |
| 62 | width: 300, |
| 63 | height: 300 |
| 64 | }) |
| 65 | const stencilProps = ref({ |
| 66 | handlers: {}, |
| 67 | movable: false, |
| 68 | scalable: false, |
| 69 | aspectRatio: 1 |
| 70 | }) |
| 71 | const resizeImage = ref({ |
| 72 | adjustStencil: false |
| 73 | }) |
| 74 | |
| 75 | function closeCropper() { |
| 76 | showCropper.value = false |
| 77 | img.value = "" |
| 78 | } |
| 79 | function openCropper() { |
| 80 | showCropper.value = true |
| 81 | img.value = "" |
| 82 | } |
| 83 | |
| 84 | function setImage(data: { |
| 85 | file: UploadSettledFileInfo |
| 86 | fileList: UploadSettledFileInfo[] |
| 87 | event: ProgressEvent | Event | undefined |
| 88 | }): void { |
| 89 | if (data?.file?.file) { |
| 90 | const reader = new FileReader() |
| 91 | reader.readAsDataURL(data.file.file) |
| 92 | reader.onload = () => { |
| 93 | img.value = reader.result?.toString() || "" |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | function emitCrop() { |
| 99 | if (cropper.value) { |
| 100 | const result = cropper.value.getResult() |
| 101 | emit("crop", result) |
| 102 | } |
| 103 | closeCropper() |
| 104 | } |
| 105 | </script> |
| 106 | |
| 107 | <style lang="scss" scoped> |
| 108 | .image-cropper-modal { |
| 109 | width: 90vw; |
| 110 | max-width: 300px; |
| 111 | |
| 112 | .upload-box { |
| 113 | height: 100%; |
| 114 | width: 100%; |
| 115 | |
| 116 | .n-upload { |
| 117 | height: 100%; |
| 118 | width: 100%; |
| 119 | |
| 120 | :deep() { |
| 121 | .n-upload-trigger, |
| 122 | .n-upload-dragger { |
| 123 | height: 100%; |
| 124 | width: 100%; |
| 125 | display: flex; |
| 126 | justify-content: center; |
| 127 | align-items: center; |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | .crop-box { |
| 134 | overflow: hidden; |
| 135 | border-radius: var(--border-radius-small); |
| 136 | |
| 137 | :deep() { |
| 138 | .vue-line-wrapper { |
| 139 | display: none; |
| 140 | } |
| 141 | } |
| 142 | } |
| 143 | } |
| 144 | </style> |