Back to questions
Middle+
4
Техническое задание: компонент для кропа изображения Цель: Разработать собственный компонент с нуля (Vue + TypeScript), который позволит пользователю загружать изображение, выбирать область для обрезки и получать результат в виде координат. Требования к функционалу: 1. Отдельный компонент - Компонент должен быть полностью самостоятельным, написан с нуля. - Использовать Vue и TypeScript. 2. Загрузка и отображение изображения - Предусмотреть окно (область просмотра), куда можно вставить изображение. - У области должны быть заданные максимальные ширина и высота. - Если изображение превышает размеры области, оно автоматически масштабируется под размер окна (без выхода за границы экрана). 3. Выбор области для кропа - После загрузки изображение отображается в окне. - Пользователь должен иметь возможность мышкой выбрать область для обрезки. - произвольная область (без фиксированного соотношения сторон). 4. Сохранение результата - Кнопка «Сохранить» вызывает событие или метод. - В результате возвращаются координаты выбранной области кропа. - Сам процесс обрезки на сервере реализовывать не требуется.
Companies where asked
TirScript
Answer from AI
sobes.tech 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 для получения координат кропа и дальнейшей обработки на сервере или клиенте.