Middle+
Technische vereiste: component voor het bijsnijden van afbeeldingen Doel: Ontwikkel een eigen component vanaf nul (Vue + TypeScript) waarmee de gebruiker een afbeelding kan uploaden, een gebied voor bijsnijden kan selecteren en het resultaat in de vorm van coördinaten kan krijgen. Functionele vereisten: 1. Onafhankelijk component - Het component moet volledig zelfstandig zijn, vanaf nul geschreven. - Gebruik Vue en TypeScript. 2. Uploaden en weergeven van afbeelding - Voorzie een venster (voorbeeldgebied) waarin een afbeelding kan worden geplaatst. - Het gebied moet een maximale breedte en hoogte hebben. - Als de afbeelding de afmetingen van het gebied overschrijdt, wordt deze automatisch geschaald om aan te passen aan de grootte van het venster (zonder de schermranden te overschrijden). 3. Selectie van het gebied voor bijsnijden - Na het uploaden wordt de afbeelding in het venster weergegeven. - De gebruiker moet de mogelijkheid hebben om met de muis een gebied voor bijsnijden te selecteren. - willekeurig gebied (zonder vaste aspectverhouding). 4. Resultaat opslaan - De knop "Opslaan" activeert een gebeurtenis of methode. - Als resultaat worden de coördinaten van het geselecteerde bijsnijdgebied geretourneerd. - Het uitvoeren van het bijsnijdproces op de server is niet vereist.
sobes.tech AI
Antwoord van AI
Ниже пример простого Vue 3 компонента на TypeScript, который реализует загрузку изображения, масштабирование под заданные размеры, выбор произвольной области для кропа и возврат координат выбранной области при сохранении.
<template>
<div class="crop-container" :style="containerStyle">
<input type="file" accept="image/*" @change="onFileChange" />
<div
v-if="imageSrc"
class="image-wrapper"
ref="imageWrapper"
@mousedown="startSelection"
@mousemove="moveSelection"
@mouseup="endSelection"
@mouseleave="endSelection"
>
<img :src="imageSrc" ref="image" :style="imageStyle" />
<div
v-if="selection"
class="selection"
:style="selectionStyle"
></div>
</div>
<button @click="saveCrop" :disabled="!selection">Сохранить</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, computed } from 'vue';
export default defineComponent({
name: 'ImageCropper',
emits: ['save'],
setup(_, { emit }) {
const MAX_WIDTH = 400;
const MAX_HEIGHT = 300;
const imageSrc = ref<string | null>(null);
const image = ref<HTMLImageElement | null>(null);
const imageWrapper = ref<HTMLDivElement | null>(null);
const containerStyle = {
width: `${MAX_WIDTH}px`,
height: `${MAX_HEIGHT}px`,
border: '1px solid #ccc',
position: 'relative',
userSelect: 'none',
};
const imageStyle = reactive({
maxWidth: '100%',
maxHeight: '100%',
display: 'block',
userSelect: 'none',
pointerEvents: 'none',
});
const selection = reactive<{ x: number; y: number; width: number; height: number } | null>(null);
const isSelecting = ref(false);
const startPoint = reactive({ x: 0, y: 0 });
function onFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = e => {
imageSrc.value = e.target?.result as string;
selection.x = 0;
selection.y = 0;
selection.width = 0;
selection.height = 0;
};
reader.readAsDataURL(file);
}
}
function getRelativeCoords(event: MouseEvent) {
if (!imageWrapper.value) return { x: 0, y: 0 };
const rect = imageWrapper.value.getBoundingClientRect();
let x = event.clientX - rect.left;
let y = event.clientY - rect.top;
// Ограничиваем координаты внутри контейнера
x = Math.max(0, Math.min(x, rect.width));
y = Math.max(0, Math.min(y, rect.height));
return { x, y };
}
function startSelection(event: MouseEvent) {
if (!imageSrc.value) return;
isSelecting.value = true;
const coords = getRelativeCoords(event);
startPoint.x = coords.x;
startPoint.y = coords.y;
selection.x = coords.x;
selection.y = coords.y;
selection.width = 0;
selection.height = 0;
}
function moveSelection(event: MouseEvent) {
if (!isSelecting.value || !selection) return;
const coords = getRelativeCoords(event);
selection.x = Math.min(coords.x, startPoint.x);
selection.y = Math.min(coords.y, startPoint.y);
selection.width = Math.abs(coords.x - startPoint.x);
selection.height = Math.abs(coords.y - startPoint.y);
}
function endSelection() {
isSelecting.value = false;
}
const selectionStyle = computed(() => {
if (!selection) return {};
return {
position: 'absolute',
border: '2px dashed #007bff',
left: `${selection.x}px`,
top: `${selection.y}px`,
width: `${selection.width}px`,
height: `${selection.height}px`,
pointerEvents: 'none',
backgroundColor: 'rgba(0, 123, 255, 0.2)',
};
});
function saveCrop() {
if (!selection) return;
// Возвращаем координаты выбранной области
emit('save', {
x: Math.round(selection.x),
y: Math.round(selection.y),
width: Math.round(selection.width),
height: Math.round(selection.height),
});
}
return {
imageSrc,
image,
imageWrapper,
containerStyle,
imageStyle,
selection,
selectionStyle,
onFileChange,
startSelection,
moveSelection,
endSelection,
saveCrop,
};
},
});
</script>
<style scoped>
.crop-container {
font-family: Arial, sans-serif;
}
.image-wrapper {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
cursor: crosshair;
margin-top: 10px;
}
.selection {
box-sizing: border-box;
}
button {
margin-top: 10px;
padding: 6px 12px;
font-size: 14px;
}
</style>
Объяснение:
- Компонент позволяет загрузить изображение через input.
- Изображение масштабируется под максимальные размеры контейнера.
- Пользователь мышкой выделяет произвольную область (без фиксированного соотношения).
- При нажатии кнопки «Сохранить» эмитируется событие
saveс координатами выбранной области.
Такой компонент можно использовать в любом Vue-приложении, подписавшись на событие save для получения координат кропа и дальнейшей обработки на сервере или клиенте.