zhangwencui
14 小时以前 617ad108a3c7f4e676229b1495185e66fac644b7
src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue
@@ -1,306 +1,324 @@
<template>
  <div class="carousel-cards">
    <button
      v-if="canScrollLeft"
      class="nav-button nav-button-left"
      @click="scrollLeftFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="左箭头" />
    <button v-if="canScrollLeft"
            class="nav-button nav-button-left"
            @click="scrollLeftFn">
      <img src="@/assets/BI/jiantou.png"
           alt="左箭头" />
    </button>
    <div
      class="cards-container"
      :style="{ '--visible-count': visibleCount }"
      ref="cardsContainerRef"
    >
      <div
        v-for="(item, index) in items"
        :key="index"
        class="card-item"
      >
        <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div>
    <div class="cards-container"
         :style="{ '--visible-count': visibleCount }"
         ref="cardsContainerRef">
      <div v-for="(item, index) in items"
           :key="index"
           class="card-item">
        <div v-if="item.icon"
             class="card-icon"
             :style="{ backgroundImage: `url(${item.icon})` }"></div>
        <div class="card-title">
          <div class="card-label">{{ item.label }}</div>
          <div class="card-value">
            <span class="value-number">{{ item.value }}</span>
            <span class="value-unit">{{ item.unit }}</span>
          </div>
          <div v-if="item.rate ?? item.ratio ?? item.percent" class="card-rate">
          <div v-if="item.rate ?? item.ratio ?? item.percent"
               class="card-rate">
            <span class="rate-value">{{ item.rate ?? item.ratio ?? item.percent }}%</span>
          </div>
        </div>
      </div>
    </div>
    <button
      v-if="canScrollRight"
      class="nav-button nav-button-right"
      @click="scrollRightFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="右箭头" />
    <button v-if="canScrollRight"
            class="nav-button nav-button-right"
            @click="scrollRightFn">
      <img src="@/assets/BI/jiantou.png"
           alt="右箭头" />
    </button>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
  import {
    ref,
    onMounted,
    onBeforeUnmount,
    nextTick,
    watch,
    computed,
  } from "vue";
const props = defineProps({
  items: {
    type: Array,
    default: () => [],
    validator: (value) => {
      return value.every(item =>
        item && typeof item.label !== 'undefined' &&
        typeof item.value !== 'undefined' &&
        typeof item.unit !== 'undefined'
      )
    }
  },
  visibleCount: {
    type: Number,
    default: 3
  }
})
  const props = defineProps({
    items: {
      type: Array,
      default: () => [],
      validator: value => {
        return value.every(
          item =>
            item &&
            typeof item.label !== "undefined" &&
            typeof item.value !== "undefined" &&
            typeof item.unit !== "undefined"
        );
      },
    },
    visibleCount: {
      type: Number,
      default: 3,
    },
  });
const cardsContainerRef = ref(null)
const currentScrollLeft = ref(0)
const maxScrollLeft = ref(0)
  const cardsContainerRef = ref(null);
  const currentScrollLeft = ref(0);
  const maxScrollLeft = ref(0);
// 检查是否可以向左滚动
const canScrollLeft = computed(() => {
  return currentScrollLeft.value > 0
})
  // 检查是否可以向左滚动
  const canScrollLeft = computed(() => {
    return currentScrollLeft.value > 0;
  });
// 检查是否可以向右滚动
const canScrollRight = computed(() => {
  return currentScrollLeft.value < maxScrollLeft.value
})
  // 检查是否可以向右滚动
  const canScrollRight = computed(() => {
    return currentScrollLeft.value < maxScrollLeft.value;
  });
// 更新滚动状态
const updateScrollState = () => {
  const container = cardsContainerRef.value
  if (!container) return
  currentScrollLeft.value = container.scrollLeft
  maxScrollLeft.value = container.scrollWidth - container.clientWidth
}
  // 更新滚动状态
  const updateScrollState = () => {
    const container = cardsContainerRef.value;
    if (!container) return;
// 向左滚动
const scrollLeftFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: -scrollDistance,
    behavior: 'smooth'
  })
  // 延迟更新状态,等待滚动动画完成
  setTimeout(() => {
    updateScrollState()
  }, 300)
}
    currentScrollLeft.value = container.scrollLeft;
    maxScrollLeft.value = container.scrollWidth - container.clientWidth;
  };
// 向右滚动
const scrollRightFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: scrollDistance,
    behavior: 'smooth'
  })
  // 延迟更新状态,等待滚动动画完成
  setTimeout(() => {
    updateScrollState()
  }, 300)
}
  // 向左滚动
  const scrollLeftFn = () => {
    const container = cardsContainerRef.value;
    if (!container) return;
// 监听 items 变化,更新滚动状态
watch(() => props.items, () => {
  nextTick(() => {
    updateScrollState()
  })
}, { deep: true })
    const scrollItems = Array.from(container.querySelectorAll(".card-item"));
    if (scrollItems.length === 0) return;
onMounted(() => {
  nextTick(() => {
    updateScrollState()
    // 监听滚动事件
    const container = cardsContainerRef.value
    const itemWidth = scrollItems[0]?.offsetWidth || 0;
    const gap = 12;
    const scrollDistance = itemWidth + gap;
    container.scrollBy({
      left: -scrollDistance,
      behavior: "smooth",
    });
    // 延迟更新状态,等待滚动动画完成
    setTimeout(() => {
      updateScrollState();
    }, 300);
  };
  // 向右滚动
  const scrollRightFn = () => {
    const container = cardsContainerRef.value;
    if (!container) return;
    const scrollItems = Array.from(container.querySelectorAll(".card-item"));
    if (scrollItems.length === 0) return;
    const itemWidth = scrollItems[0]?.offsetWidth || 0;
    const gap = 12;
    const scrollDistance = itemWidth + gap;
    container.scrollBy({
      left: scrollDistance,
      behavior: "smooth",
    });
    // 延迟更新状态,等待滚动动画完成
    setTimeout(() => {
      updateScrollState();
    }, 300);
  };
  // 监听 items 变化,更新滚动状态
  watch(
    () => props.items,
    () => {
      nextTick(() => {
        updateScrollState();
      });
    },
    { deep: true }
  );
  onMounted(() => {
    nextTick(() => {
      updateScrollState();
      // 监听滚动事件
      const container = cardsContainerRef.value;
      if (container) {
        container.addEventListener("scroll", updateScrollState);
      }
    });
  });
  onBeforeUnmount(() => {
    // 清理滚动事件监听器
    const container = cardsContainerRef.value;
    if (container) {
      container.addEventListener('scroll', updateScrollState)
      container.removeEventListener("scroll", updateScrollState);
    }
  })
})
onBeforeUnmount(() => {
  // 清理滚动事件监听器
  const container = cardsContainerRef.value
  if (container) {
    container.removeEventListener('scroll', updateScrollState)
  }
})
  });
</script>
<style scoped>
.carousel-cards {
  width: 100%;
  overflow: hidden;
  position: relative;
  display: flex;
  align-items: center;
}
  .carousel-cards {
    width: 100%;
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: center;
  }
.cards-container {
  display: flex;
  gap: 12px;
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE and Edge */
  padding-bottom: 4px;
  scroll-behavior: smooth;
}
  .cards-container {
    display: flex;
    gap: 12px;
    width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* IE and Edge */
    padding-bottom: 4px;
    scroll-behavior: smooth;
  }
.cards-container::-webkit-scrollbar {
  display: none; /* Chrome, Safari, Opera */
}
  .cards-container::-webkit-scrollbar {
    display: none; /* Chrome, Safari, Opera */
  }
.nav-button {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  background: rgba(26, 88, 176, 0.6);
  border: 1px solid rgba(26, 88, 176, 0.8);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 10;
  transition: all 0.3s ease;
  padding: 0;
}
  .nav-button {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 32px;
    height: 32px;
    background: rgba(26, 88, 176, 0.6);
    border: 1px solid rgba(26, 88, 176, 0.8);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 10;
    transition: all 0.3s ease;
    padding: 0;
  }
.nav-button:hover {
  background: rgba(26, 88, 176, 0.8);
  transform: translateY(-50%) scale(1.1);
}
  .nav-button:hover {
    background: rgba(26, 88, 176, 0.8);
    transform: translateY(-50%) scale(1.1);
  }
.nav-button-left {
  left: -16px;
}
  .nav-button-left {
    left: -16px;
  }
.nav-button-left img {
  width: 16px;
  height: 16px;
  transform: rotate(180deg);
}
  .nav-button-left img {
    width: 16px;
    height: 16px;
    transform: rotate(180deg);
  }
.nav-button-right {
  right: -16px;
}
  .nav-button-right {
    right: -16px;
  }
.nav-button-right img {
  width: 16px;
  height: 16px;
}
  .nav-button-right img {
    width: 16px;
    height: 16px;
  }
.card-item {
  flex: 0 0 calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  display: flex;
  align-items: center;
  background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
  border-radius: 8px 8px 8px 8px;
  padding: 12px 16px;
  transition: all 0.3s ease;
}
  .card-item {
    flex: 0 0
      calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
    min-width: calc(
      (100% - (var(--visible-count) - 1) * 12px) / var(--visible-count)
    );
    display: flex;
    align-items: center;
    background: linear-gradient(
      269deg,
      rgba(27, 57, 126, 0.13) 0%,
      rgba(33, 137, 206, 0.33) 98.13%,
      #24aff4 100%
    );
    border-radius: 8px 8px 8px 8px;
    padding: 12px 16px;
    transition: all 0.3s ease;
  }
.card-item:hover {
  transform: translateY(-2px);
}
  .card-item:hover {
    transform: translateY(-2px);
  }
.card-icon {
  width: 80px;
  height: 60px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  flex-shrink: 0;
  margin-right: 12px;
}
  .card-icon {
    width: 80px;
    height: 60px;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    flex-shrink: 0;
    margin-right: 12px;
  }
.card-title {
  display: flex;
  align-items: flex-start;
  flex-direction: column;
  flex: 1;
}
  .card-title {
    display: flex;
    align-items: flex-start;
    flex-direction: column;
    flex: 1;
  }
.card-label {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
}
  .card-label {
    font-weight: 400;
    font-size: 14px;
    color: #ffffff;
    margin-bottom: 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    width: 100%;
  }
.card-value {
  display: flex;
  align-items: baseline;
  gap: 4px;
}
  .card-value {
    display: flex;
    align-items: baseline;
    gap: 4px;
  }
.card-rate {
  margin-top: 4px;
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.85);
}
  .card-rate {
    margin-top: 4px;
    display: flex;
    align-items: center;
    gap: 6px;
    font-weight: 400;
    font-size: 12px;
    color: rgba(255, 255, 255, 0.85);
  }
.rate-label {
  opacity: 0.85;
}
  .rate-label {
    opacity: 0.85;
  }
.rate-value {
  font-weight: 500;
}
  .rate-value {
    font-weight: 500;
  }
.value-number {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  line-height: 1;
}
  .value-number {
    font-weight: 400;
    font-size: 14px;
    color: #ffffff;
    line-height: 1;
  }
.value-unit {
  font-size: 14px;
  color: #FFFFFF;
  font-weight: 400;
}
  .value-unit {
    font-size: 14px;
    color: #ffffff;
    font-weight: 400;
  }
</style>