Верстка (функционал) вложенных коментариев с палочкой относительности к родителю

Пытаюсь сделать задачу - нужно сделать адаптивную палочку от дочерних комментариев к родителю, есть пять уровней вложенности, палочка должна быть до самого верха через компоненты проходить. Vue 3. В компоненте коммента пиксели умножаются на уровень вложенности и получается отступ. Как это лучше сделать, более грамотно? Заранее благодарю!

Приложу компонент родителя и коммента ниже, мб поможет.

Родитель:

<template>
  <div class="Comments-List">
    <template v-if="get_isReady">
      <p class="Comments-List__ttl-separator" v-if="get_isShowLastCommentsTtl">Последние комментарии</p>
      <CommentItem
        class="Comments-List__item"
        v-for="(comment, id, index) in get_comments"
        :style="getMargin(comment.level)"
        :key="comment.id"
        :comment="comment"
        :commentsPlace="commentsPlace"
        :context="props.context"
      />
      <div class="Comments-List__next-comment-ldr" v-if="get_isLoadingNewComments">
        <LoaderCircle :color="'var(--blue-600)'" :size="24"/>
      </div>
    </template>
    <div class="Comments-List__ldr" v-else>
      <LoaderCircle :color="'var(--blue-600-s)'" :size="32"/>
    </div>
  </div>
</template>

<script setup>

//---------------------------
const props = defineProps(Props());
const store = useStore();
//---------------------------
onMounted(async () => {
  await toLoadComments();
  await nextTick();
  goToTheComment();
});
//---------------------------
//Получить контекст объекта для комментариев
const get_commentsCtx = computed(() => {
  if (props.commentsPlace === 'popup') return 'popupComments';
  else if (props.commentsPlace === 'page') return 'contentPageComments';
})
//Готов ли список к отрисовке
const get_isReady = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].isReady);
//Получить список комментариев
const get_comments = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].list);
//Отображать ли заголовок последних комментариев?
const get_isShowLastCommentsTtl = computed(() => get_comments.value?.length > 0);
//Идет ли подгрузка новых комментов
const get_isLoadingNewComments = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].isLoadingNext);
//---------------------------
//Получить комментарии
const toLoadComments = async () => {
  await store.dispatch('ContentSystem/Comments/startCommentsLoadingProcess', {postId: props.postId, context: get_commentsCtx.value, area: props.context});
  return;
}
//Получить отступ коммента относительно его уровня вложенности
const getMargin = (level) => {
  const levelNum = parseInt(level);
  return `
      margin-top: ${levelNum === 0 ? 22 : 10}px;
      margin-left: ${levelNum * 30}px;
    `;
}
//Проскролить к конкретному комментарию
const goToTheComment = async () => {
  const idCommentScroll = store.state.ContentSystem.Comments[get_commentsCtx.value].targetCommentsId;
  if (!idCommentScroll) return;
  await nextTick();
  const widget = document.querySelector('.Wdgt-Content');
  const comment = widget.querySelector(`[data-comment-id="${idCommentScroll}"]`);
  if (!comment) return;
  comment.scrollIntoView({
    behavior: 'auto',
    block: 'center'
  });
  store.commit('ContentSystem/Comments/setScrollTarget', {
    isComments: null,
    idComment: null,
    context: get_commentsCtx.value
  });
}
</script>

<style lang="scss">
@import './styles/_styles';
</style>

Коммент:

<template>
  <div class="Comment-Item" :data-comment-id="props.comment.id">

    <router-link @click="toCloseAllPopups" class="Comment-Item__avt" :to="{ name: PATHS_NAME.OTHER_USERS.PROFILE, params: { username: props.comment.user.username } }">
      <div v-if="props.comment.level > 0" class="dependency-line">

      </div>
      <AvatarImage :src="props.comment.user.avatar" :size="24" :alt="'name'"/>
    </router-link>
    <router-link @click="toCloseAllPopups" class="Comment-Item__name" :to="{ name: PATHS_NAME.OTHER_USERS.PROFILE, params: { username: props.comment.user.username } }">
      {{ props.comment.user.name }}
    </router-link>
    <CommentMenu class="Comment-Item__menu" :commentsPlace="props.commentsPlace" :commentId="props.comment.id" v-if="get_isShowContextMenu"/>
    <div v-if="props.comment.nested_count > 0" class="dependency-line-top">
      <div class="line"></div>
    </div>
    <p :class="{'Comment-Item__txt-hidden': !isExpanded}" class="Comment-Item__txt" v-if="!get_isOpenEditForm && !comment.is_removed">
      <span>{{ isExpanded ? props.comment.comment : truncatedComment }}</span>
    </p>
    <div v-if="get_isTruncated" class="Comment-Item__text-wrapper">
      <button class="Comment-Item__show-more" @click="toggleExpand">
        {{ isExpanded ? 'Скрыть' : 'Показать полностью' }}
      </button>
    </div>
    <p class="Comment-Item__removed-txt" v-if="comment.is_removed">
      <InfoIcon class="Comment-Item__info-icon"/>
      Комментарий удалён автором или оракулом клуба...
      <button class="Comment-Item__return-comment" @click="returnComment">Восстановить</button>
    </p>
    <img class="Comment-Item__img" :src="props.comment.image_url" @click="handleClickToContent" v-if="get_isShowImage" alt="img"/>
    <template v-if="!BASIC_VIEW">
      <CommentsForm
        class="Comment-Item__form-edit"
        :commentsPlace="props.commentsPlace"
        :selfId="props.comment.id"
        :isFocus="true"
        :mode="'edit_comment'"
        :postId="get_objectId"
        :commentId="0"
        :context="props.context"
        v-if="get_isOpenEditForm"
      />
    </template>
    <div class="Comment-Item__ftr">
      <span class="Comment-Item__edited" v-if="props.comment.is_edited">Ред</span>
      <time class="Comment-Item__time" :class="get_isWithoutPoint">{{ get_formatedTime }}</time>
      <button class="Comment-Item__answer" @click="goToComment" v-if="BASIC_VIEW">Ответить</button>
      <button class="Comment-Item__answer" @click="toToggleForm">{{ get_textButton }}</button>
      <CommentAddReaction class="Comment-Item__add-react" :comment="props.comment" :commentsPlace="get_commentsCtx" v-if="!BASIC_VIEW"/>
    </div>
    <CommentReactions class="Comment-Item__reactions" :comment="props.comment" :commentsPlace="get_commentsCtx" v-if="!BASIC_VIEW"/>
    <template v-if="!BASIC_VIEW">
      <CommentsForm
        class="Comment-Item__form"
        :commentsPlace="props.commentsPlace"
        :selfId="props.comment.level === 5 ? props.comment.parent_id : props.comment.id"
        :isFocus="true"
        :mode="'answer'"
        :postId="get_objectId"
        :context="props.context"
        v-if="get_isOpenNewCommentForm"
      />
    </template>
  </div>

</template>

<script setup>
//---------------------------
const props = defineProps(Props());
const store = useStore();
const route = useRoute();
//---------------------------
onBeforeUnmount(() => {
  if (!get_isOpenForm.value) return;
  store.commit('ContentSystem/Comments/setChildFormCommentId', {id: null, context: get_commentsCtx.value});
})
//---------------------------
const BASIC_VIEW = props.basicView;
const MAX_VISIBLE_SYMBOLS = 500;
const POST = store.state.ContentSystem.Alpha.list[props.comment.object_pk] || store.state.ContentSystem.Infonoise.list[props.comment.object_pk];
//---------------------------
//Состояние, управляющее развернутым видом комментария
const isExpanded = ref(false);
//---------------------------
//Проверяем, нужно ли обрезать текст (более MAX_VISIBLE_SYMBOLS символов)
const get_isTruncated = computed(() => props.comment.comment.length > MAX_VISIBLE_SYMBOLS);
//Если текст нужно обрезать, возвращаем первые MAX_VISIBLE_SYMBOLS символов
const truncatedComment = computed(() => get_isTruncated.value ? `${props.comment.comment.slice(0, MAX_VISIBLE_SYMBOLS)}...` : props.comment.comment);
//Получить контекст объекта для комментариев
const get_commentsCtx = computed(() => {
  if (props.commentsPlace === 'popup') return 'popupComments';
  else if (props.commentsPlace === 'page') return 'contentPageComments';
})
//Получить id, к которому относится комментарий
const get_objectId = computed(() => parseInt(props.comment.object_pk));
//Получить id комментария, на которого планируется ответ
const get_idCommentForm = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].childFormCommentId);
//Комментарий находится в режиме редактирования?
const get_isEditCommentMode = computed(() => store.state.ContentSystem.Comments[get_commentsCtx.value].isEditMode);
//Открыта ли форма для этого элемента
const get_isOpenForm = computed(() => get_idCommentForm.value === props.comment.id);
//Открыта форма для создания коммента?
const get_isOpenNewCommentForm = computed(() => get_isOpenForm.value && !get_isEditCommentMode.value);
//Открыта форма для редактировани?
const get_isOpenEditForm = computed(() => get_isOpenForm.value && get_isEditCommentMode.value);
//Получить форматированное время
const get_formatedTime = computed(() => formatTime_dn_mn_y_h_min(props.comment.submit_date));
//Получить текст для кнопки
const get_textButton = computed(() => get_isOpenForm.value ? 'Скрыть' : 'Ответить');
//Получить адрес перехода
const get_routeUrl = computed(() => `/${POST.model_name.toLowerCase()}/${get_objectId.value}`);
//Отображать ли кнопку ответить?
const get_isShowAnswerBtn = computed(() => props.comment.level <= 5);
//Не ставить точку после элемента
const get_isWithoutPoint = computed(() => BASIC_VIEW || !BASIC_VIEW && get_isShowAnswerBtn.value ? '' : 'Comment-Item__time--without-point');
//Отобразить ли изображение?
const get_isShowImage = computed(() => props.comment.image_url && !get_isOpenEditForm.value);
//Это комментарий пользователя?
const get_isMine = computed(() => {
  const userName = store.state.UserSystem.user.username;
  return userName === props.comment.user.username;
})
//Отображать ли кнопку удаления?
const get_isShowContextMenu = computed(() => {
  return (get_isMine.value || store.getters['UserSystem/hasModeratorPrivileges']) && !BASIC_VIEW;
})
//---------------------------
// Функция для переключения состояния
const toggleExpand = () => {
  isExpanded.value = !isExpanded.value;
};
//Переключить видимость формы
const toToggleForm = () => {
  get_isOpenForm.value ?
    store.commit('ContentSystem/Comments/setChildFormCommentId', {id: null, context: get_commentsCtx.value}) :
    store.commit('ContentSystem/Comments/setChildFormCommentId', {
      id: props.comment.id,
      context: get_commentsCtx.value
    });
}

//Восстановить комментарий
const returnComment = async () => {
  try {
    await restoreComment(props.comment.id);
    const comment = store.state.ContentSystem.Comments[get_commentsCtx.value].list.find(item => item.id == props.comment.id);
    comment.is_removed = false;
    POST.comments_count++;
  } catch (error) {

  } finally {

  }
}
//Переместиться к комментарию
const goToComment = async () => {
  store.dispatch('Dropdowns/removeTheDropdown', DROPDOWN_NAMES.contentPage);
  await nextTick();
  toChangeUrl();
  store.commit('ContentSystem/Comments/setScrollTarget', {
    isComments: true,
    idComment: props.comment.id,
    context: get_commentsCtx.value
  });
  store.dispatch('Dropdowns/addNewDropdown', DROPDOWN_NAMES.contentPage);
}
//Обновить адрес в строке поиска и запомнить последний урл перед переходом
const toChangeUrl = () => {
  store.commit('ContentSystem/toRememberLastPageUrl', route.path);
  window.history.pushState({}, '', window.location.href);
  window.history.replaceState({}, '', get_routeUrl.value);
};
//Клик по контентной области
const handleClickToContent = (e) => {
  if (e.target.tagName === 'IMG') {
    openSlider([{src: e.target.src, text: null}]);
  }
}
//Открыть слайдер с картинкой
const openSlider = (data) => {
  store.commit('SliderSystem/toWriteSlides', data);
  store.commit('SliderSystem/toChooseSlide', 0);
  store.dispatch('Dropdowns/addNewDropdown', DROPDOWN_NAMES.slider);
}
//Очистить все дропдауны при переходе на страницу
const toCloseAllPopups = () => {
  store.dispatch('Dropdowns/clearAllDropdowns');
}
</script>

<style lang="scss">

@import './styles/_styles';
</style>

Из фигмы пример Из фигмы пример

Попытки сделать попытки сделать


Ответы (0 шт):