Sobes.tech
Назад к вопросам
Middle+
2

Техническое задание: компонент для кропа изображения Цель: Разработать собственный компонент с нуля (Vue + TypeScript), который позволит пользователю загружать изображение, выбирать область для обрезки и получать результат в виде координат. Требования к функционалу: 1. Отдельный компонент - Компонент должен быть полностью самостоятельным, написан с нуля. - Использовать Vue и TypeScript. 2. Загрузка и отображение изображения - Предусмотреть окно (область просмотра), куда можно вставить изображение. - У области должны быть заданные максимальные ширина и высота. - Если изображение превышает размеры области, оно автоматически масштабируется под размер окна (без выхода за границы экрана). 3. Выбор области для кропа - После загрузки изображение отображается в окне. - Пользователь должен иметь возможность мышкой выбрать область для обрезки. - произвольная область (без фиксированного соотношения сторон). 4. Сохранение результата - Кнопка «Сохранить» вызывает событие или метод. - В результате возвращаются координаты выбранной области кропа. - Сам процесс обрезки на сервере реализовывать не требуется.

Компании, где спрашивали
TirScript

Ответ от нейросети

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 для получения координат кропа и дальнейшей обработки на сервере или клиенте.