Vue3 + Ts 封装点击切换,左右滚动的商品展示组件

该代码展示了一个使用Vue.js编写的轮播图组件,具有左右按钮控制图片切换的功能。图片列表可以通过`imgList`进行设置,组件内部通过计算样式和事件监听实现了平滑的滚动效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

效果图

点击左右按钮 图片均可实现左右滚动切换效果
在这里插入图片描述

直接上代码

<template>
  <div class="details">
    <div class="swiperBox">
      <el-carousel :autoplay="false" ref="swiperRef">
        <el-carousel-item
          v-for="(item, index) in imgList"
          :key="index"
          :name="`${index}`"
        >
          <img style="width: 100%; height: 100%" :src="item" />
        </el-carousel-item>
      </el-carousel>
    </div>
    <div class="arrowBox">
      <div
        class="iconBox"
        :style="swiperAct == 0 ? 'pointer-events: none' : ''"
        @click="arrowBold('left')"
      >
        <el-icon><ArrowLeftBold /></el-icon>
      </div>
      <div class="imgWrap">
        <div class="imgBox" id="imgBox" ref="imgBoxRef">
          <div
            class="img"
            :id="`img${index}`"
            :style="
              swiperAct == index
                ? 'border: 2px solid #eb0045;'
                : 'border: 2px solid #f8f8f8;'
            "
            v-for="(item, index) in imgList"
            :key="index"
            @click="arrowClick(index)"
          >
            <img style="width: 100%; height: 100%" :src="item" />
          </div>
        </div>
      </div>
      <div
        class="iconBox"
        :style="swiperAct == imgList.length - 1 ? 'pointer-events: none' : ''"
        @click="arrowBold('right')"
      >
        <el-icon><ArrowRightBold /></el-icon>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive, toRefs, ref, onMounted, onUnmounted } from "vue";

const swiperRef = ref();
const imgBoxRef = ref();

let data = reactive({
  imgList: [
    "https://5b0988e595225.cdn.sohucs.com/images/20200430/fe07a3826cb548ad9d9dd1ac1b59b26e.jpeg",
    "https://p4.itc.cn/images01/20210414/348f8b1417d3490891ae8cefda30466f.jpeg",
    "https://image.uc.cn/s/wemedia/s/upload/2021/496ac52bdaab446a7993e6507ecef696.jpg",
    "https://gss0.baidu.com/-Po3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/ae51f3deb48f8c546fe467cd3b292df5e0fe7f19.jpg",
    "https://p3.toutiaoimg.com/origin/pgc-image/8bb2a8d88e474410b2fd9bc0106c7425?from=pc",
    "https://p7.itc.cn/images01/20201001/501434b4b09a4500bad79cb401572fc5.jpeg",
    "https://img0.baidu.com/it/u=1275095085,1961143463&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800",
    "https://img2.baidu.com/it/u=1966616150,2146512490&fm=253&fmt=auto&app=120&f=JPEG?w=751&h=500",
    "https://img0.baidu.com/it/u=1049144354,3589714554&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800",
  ],
  swiperAct: 0,
  isScoll:true
});

let { imgList, swiperAct,isScoll } = toRefs(data);
// 鼠标点击事件
let clickHandle = (e: any) => {
  if (e.x <= 518) {
    // 调用方法向左滑动
    scrollRight("left");
  }
  if (e.x >= 660) {
    // 调用方法向右滑动
    scrollRight("right");
  }
};
// 选中的图片索引
let arrowClick = (index: any) => {
  swiperRef.value.setActiveItem(index);
  swiperAct.value = index;
};
// 上下切换图片
let arrowBold = (type: any) => {
  let value: any;
  if (type == "left") {
    value = swiperAct.value - 1;
    scrollRight("left");
  }
  if (type == "right") {
    value = swiperAct.value + 1;
    scrollRight("right");
  }
  // 现实具体图片
  swiperRef.value.setActiveItem(value);
  // 当前现实的图片
  swiperAct.value = value;
};
/**
 * 左右移动事件
 * @param type 向左/右移动
 */
let scrollRight = (type?: any) => {
  if(!isScoll.value)return
  // 节流
  isScoll.value= false
  /**
   * @param 618 可视化宽度 最大的盒子
   * @param 5 可视化中现实数量
   * @param imgList 数组长度
   */
  const allLength = Math.floor((618 / 5) * imgList.value.length);
  // 获取包裹商品卡片的div盒子的宽度
  const boxLength = imgBoxRef.value.clientWidth;
  // 数组总长度比大盒子的宽度还要小
  if (allLength < boxLength) return;
  // 获取包裹商品卡片的div盒子
  const listEl = imgBoxRef.value;
  // 获取定位的 left 值
  const rightMove = Math.abs(parseInt(window.getComputedStyle(listEl)?.left));
  /**
   * @param rightMove 定位的 left 值
   * @param boxLength 包裹商品卡片的div盒子的宽度
   * @param 124 是单个商品卡片的宽度
   * @param allLength 可视化宽度 最大的盒子
   */
  if (rightMove + boxLength + 124 > allLength) {
    if (type == "left") {
      listEl.style.left = "-" + (rightMove - 124) + "px";
    } else {
      listEl.style.left = "-" + (allLength - boxLength) + "px";
    }
  } else {
    if (type == "left") {
      if (rightMove - 124 < 124) {
        listEl.style.left = "0px";
      } else {
        listEl.style.left = "-" + (rightMove - 124) + "px";
      }
    } else {
      listEl.style.left = "-" + (rightMove + 124) + "px";
    }
  }
  setTimeout(()=>{
  	isScoll.value= true
  },500)
};

onMounted(() => {
  // 可视化宽度 最大的盒子 添加点击事件
  imgBoxRef.value.addEventListener("click", clickHandle);
});
onUnmounted(() => {
  // 可视化宽度 最大的盒子 移除点击事件
  if (imgBoxRef.value) {
    imgBoxRef.value.removeEventListener("click", clickHandle);
  }
});
</script>

<style scoped lang="scss">
.details {
  margin-top: 50px;
  .arrowBox {
    display: flex;
    margin-top: 32px;
    height: 90px;
    align-items: center;
    width: 698px;
    .imgWrap {
      flex: 1;
      height: 90px;
      position: relative;
      margin: 0 10px;
      overflow: hidden;
      .imgBox {
        width: 100%;
        height: 90px;
        white-space: nowrap;
        position: absolute;
        left: 0px;
        transition: all 0.3s;
        .img {
          cursor: pointer;
          display: inline-block;
          width: 114px;
          height: 90px;
          margin-right: 12px;
          box-sizing: border-box;
        }
        .img:last-child {
          margin-right: 0;
        }
      }
    }
    .iconBox {
      line-height: 90px;
      width: 30px;
      height: 100%;
      background: #9f9f9f;
      color: #fff;
      text-align: center;
      cursor: pointer;
    }
    .iconBox:hover {
      background: #232836;
    }
  }
  .swiperBox {
    width: 698px;
    height: 400px;
    border-radius: 6px;
    overflow: hidden;
    :deep(.el-carousel) {
      height: 100%;
    }
    :deep(.el-carousel__container) {
      height: 100%;
    }
    :deep(.el-carousel__indicators) {
      display: none;
    }
    :deep(.el-carousel__arrow) {
      display: none;
    }
    .el-carousel__item h3 {
      color: #475669;
      opacity: 0.75;
      height: 100%;
      line-height: 400px;
      margin: 0;
      text-align: center;
    }
    .el-carousel__item:nth-child(2n) {
      background-color: #99a9bf;
    }

    .el-carousel__item:nth-child(2n + 1) {
      background-color: #d3dce6;
    }
  }
}
</style>
<think>我们将在Vue3和TypeScript环境下创建一个自定义的日期时间选择器组件,并模仿Element UI的样式。 步骤: 1. 创建组件文件:例如`DateTimePicker.vue` 2. 设计组件的props和emits,使用TypeScript定义类型 3. 使用原生HTML input元素(type="datetime-local")作为基础,但注意这个原生控件在不同浏览器样式不一,所以我们将自定义样式和交互 4. 由于要模仿Element UI的样式,我们需要参考Element UI的样式表,并尽量复制其样式 5. 处理日期时间的格式化和解析,因为原生input的值是ISO8601格式(如"2022-01-01T12:00"),我们可能需要转换为更友好的格式或直接使用Date对象 6. 使用Vue3的Composition API编写逻辑 注意:由于Element UI的样式较为复杂,我们可能无法完全复制,但可以尽量模仿其外观。 具体实现: 1. 组件结构: - 我们将使用一个输入框作为触发日期时间选择面板的按钮,并显示当前选择的日期时间。 - 当点击输入框时,显示一个自定义的日期时间选择面板(这里为了简化,我们可以使用两个原生input组合?但这样样式和交互难以控制。因此,我们考虑自己实现一个面板,但这比较复杂。作为折中,我们可以使用`<input type="date">`和`<input type="time">`组合,并自定义样式,或者使用第三方日期时间选择器库,但题目要求原生方法,所以我们自己实现面板)。 2. 考虑到时间,我们简化实现:使用一个文本输入框显示格式化的日期时间,当点击时弹出自定义面板。面板包括: - 一个日历部分(显示月份、星期、日期) - 一个时间选择部分(小时、分钟) 3. 但是,自己完全实现一个日期时间选择器是一个复杂的任务,需要大量的代码。因此,这里我们只做简单的实现,重点放在如何与Vue3和TypeScript结合,并模仿Element UI的样式。 4. 另一种思路:使用`<input type="datetime-local">`,然后通过CSS覆盖其样式,但跨浏览器样式覆盖非常困难,且无法完全控制内部样式。 5. 因此,我们决定自己构建一个简单的面板。由于完全实现一个日期时间选择器非常复杂,我们只实现基本功能,并重点放在样式模仿上。 步骤分解: 1. 创建组件框架(使用Vue3的单文件组件,语言为ts) 2. 定义props(例如:modelValue,类型为Date或字符串,这里我们使用Date对象) 3. 定义emits(update:modelValue) 4. 使用ref创建内部状态(当前选择的日期时间,面板的显示状态等) 5. 构建一个输入框,用于显示当前选择的日期时间(格式化为字符串)并触发面板 6. 构建一个浮动面板,包含日期选择和时间选择 7. 日期选择:可以切换年月,选择日期 8. 时间选择:选择小时和分钟 9. 模仿Element UI的样式,通过CSS实现 由于完整实现代码量巨大,我们将简化实现,仅展示核心概念。 以下是一个简化的实现示例(仅包含基本结构和样式,日期选择部分只做静态展示,时间选择同理):</think>在 Vue 3 和 TypeScript 环境中创建自定义日期时间选择器组件并模仿 Element UI 样式,可以按照以下步骤实现: ### 一、组件设计与实现 #### 1. 创建组件文件 在 `src/components` 下创建 `DateTimePicker.vue` 文件: ```vue <template> <div class="custom-datetime-picker"> <!-- 输入框 --> <div class="el-input" @click="togglePanel"> <input v-model="displayValue" readonly placeholder="选择日期和时间" class="el-input__inner" /> <span class="el-input__suffix"> <i class="el-icon-date"></i> </span> </div> <!-- 日期时间选择面板 --> <div v-show="showPanel" class="el-picker-panel"> <div class="el-date-picker__header"> <button @click="prevMonth">‹</button> <span>{{ currentYear }}年{{ currentMonth + 1 }}月</span> <button @click="nextMonth">›</button> </div> <!-- 日期选择区域 --> <div class="el-date-table"> <div v-for="day in calendarDays" :key="day.date" class="cell" :class="{ &#39;current&#39;: isCurrentDay(day), &#39;selected&#39;: isSelected(day) }" @click="selectDate(day)"> {{ day.day }} </div> </div> <!-- 时间选择区域 --> <div class="el-time-panel"> <div class="el-time-spinner"> <div v-for="hour in 24" :key="hour" @click="selectHour(hour - 1)" :class="{ &#39;active&#39;: selectedHour === hour - 1 }"> {{ hour - 1 }} </div> </div> <div class="el-time-spinner"> <div v-for="minute in 60" :key="minute" @click="selectMinute(minute - 1)" :class="{ &#39;active&#39;: selectedMinute === minute - 1 }"> {{ minute - 1 }} </div> </div> </div> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, computed } from &#39;vue&#39;; export default defineComponent({ name: &#39;DateTimePicker&#39;, props: { modelValue: { type: Date, default: () => new Date() } }, emits: [&#39;update:modelValue&#39;], setup(props, { emit }) { const showPanel = ref(false); const currentDate = ref<Date>(new Date(props.modelValue)); // 计算当前年月 const currentYear = computed(() => currentDate.value.getFullYear()); const currentMonth = computed(() => currentDate.value.getMonth()); // 生成当月日历 const calendarDays = computed(() => { const days = []; const firstDay = new Date(currentYear.value, currentMonth.value, 1); const lastDay = new Date(currentYear.value, currentMonth.value + 1, 0); // 填充空白 for (let i = 0; i < firstDay.getDay(); i++) { days.push({ day: &#39;&#39;, date: null }); } // 填充日期 for (let i = 1; i <= lastDay.getDate(); i++) { const date = new Date(currentYear.value, currentMonth.value, i); days.push({ day: i, date }); } return days; }); // 选择日期 const selectDate = (day: { date: Date | null }) => { if (!day.date) return; currentDate.value = new Date( day.date.getFullYear(), day.date.getMonth(), day.date.getDate(), currentDate.value.getHours(), currentDate.value.getMinutes() ); updateModelValue(); }; // 选择时间 const selectedHour = ref(currentDate.value.getHours()); const selectedMinute = ref(currentDate.value.getMinutes()); const selectHour = (hour: number) => { selectedHour.value = hour; currentDate.value.setHours(hour); updateModelValue(); }; const selectMinute = (minute: number) => { selectedMinute.value = minute; currentDate.value.setMinutes(minute); updateModelValue(); }; // 更新模型值 const updateModelValue = () => { emit(&#39;update:modelValue&#39;, new Date(currentDate.value)); }; // 其他辅助函数 const togglePanel = () => showPanel.value = !showPanel.value; const prevMonth = () => currentDate.value.setMonth(currentMonth.value - 1); const nextMonth = () => currentDate.value.setMonth(currentMonth.value + 1); const isCurrentDay = (day: { date: Date | null }) => day.date?.toDateString() === new Date().toDateString(); const isSelected = (day: { date: Date | null }) => day.date?.toDateString() === currentDate.value.toDateString(); // 显示值格式化 const displayValue = computed(() => { if (!currentDate.value) return &#39;&#39;; return `${currentDate.value.toLocaleDateString()} ${selectedHour.value}:${selectedMinute.value.toString().padStart(2, &#39;0&#39;)}`; }); return { showPanel, currentYear, currentMonth, calendarDays, selectedHour, selectedMinute, displayValue, togglePanel, prevMonth, nextMonth, selectDate, selectHour, selectMinute, isCurrentDay, isSelected }; } }); </script> <style scoped> /* 模仿Element UI样式 */ .custom-datetime-picker { position: relative; display: inline-block; } .el-input { position: relative; font-size: 14px; display: inline-block; width: 100%; } .el-input__inner { height: 40px; line-height: 40px; padding: 0 15px; border: 1px solid #dcdfe6; border-radius: 4px; width: 100%; box-sizing: border-box; } .el-input__suffix { position: absolute; right: 5px; top: 0; height: 100%; text-align: center; color: #c0c4cc; transition: all .3s; } .el-picker-panel { position: absolute; z-index: 2000; background: #fff; border: 1px solid #e4e7ed; box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); border-radius: 4px; line-height: 30px; margin: 5px 0; } .el-date-table { font-size: 12px; padding: 10px; display: grid; grid-template-columns: repeat(7, 1fr); } .cell { height: 30px; padding: 4px 0; text-align: center; cursor: pointer; } .cell:hover { background: #f2f6fc; } .current { color: #409eff; } .selected { background: #409eff; color: white; border-radius: 50%; } .el-time-panel { display: flex; border-top: 1px solid #e4e7ed; } .el-time-spinner { flex: 1; max-height: 200px; overflow-y: auto; } .el-time-spinner > div { padding: 0 10px; cursor: pointer; } .el-time-spinner > div.active { color: #409eff; font-weight: bold; } </style> ``` ### 二、组件使用示例 在父组件中使用: ```vue <template> <div> <h3>自定义日期时间选择器</h3> <DateTimePicker v-model="selectedDateTime" /> <p>选择的值:{{ selectedDateTime.toLocaleString() }}</p> </div> </template> <script lang="ts"> import { defineComponent, ref } from &#39;vue&#39;; import DateTimePicker from &#39;@/components/DateTimePicker.vue&#39;; export default defineComponent({ components: { DateTimePicker }, setup() { const selectedDateTime = ref<Date>(new Date()); return { selectedDateTime }; } }); </script> ``` ### 三、关键实现说明 1. **组件结构**: - 输入框:显示当前值并触发面板 - 日期面板:显示月份导航和日期网格 - 时间面板:显示小时和分钟选择器 2. **核心功能**: - 使用 `computed` 动态生成日历数据 - 通过 Vue 的响应式系统管理日期时间状态 - 实现日期/时间的独立选择逻辑 - 支持双向绑定 (`v-model`) 3. **样式模仿要点**: - 输入框边框和标样式 - 面板阴影和边框效果 - 日期单元格的悬停和选中状态 - 时间选择器的滚动列表样式 - 使用 Element UI 特有的颜色 (#409eff) 4. **优化建议**: - 添加动画过渡效果 - 实现面板外部点击关闭 - 增加日期范围选择功能 - 添加国际化支持 - 实现移动端适配 ### 四、注意事项 1. 当前实现是简化版本,实际生产环境需要: - 添加日期有效性验证 - 优化性能(虚拟滚动时间选择器) - 完善键盘导航支持 - 增加无障碍访问支持 2. 样式调整: - 如需完全匹配 Element Plus 样式,建议直接引用 Element 的 CSS 变量: ```css :root { --el-color-primary: #409eff; --el-border-color: #dcdfe6; } ``` - 或安装 Element Plus 后通过 SCSS 变量覆盖 ### 五、相关优化方向 1. **性能优化**: - 使用 `v-memo` 缓存静态日期单元格 - 时间选择器改用虚拟滚动 2. **功能扩展**: ```ts // 在props中添加 props: { type: { type: String as PropType<&#39;datetime&#39; | &#39;date&#39; | &#39;time&#39;>, default: &#39;datetime&#39; }, disabledDate: { type: Function as PropType<(date: Date) => boolean>, default: () => false } } ``` 3. **动画效果**: ```vue <transition name="el-zoom-in-top"> <div v-show="showPanel" class="el-picker-panel">...</div> </transition> ``` 此实现提供了基础功能框架,可根据实际需求进一步扩展功能和优化用户体验[^1][^2][^3]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷不动了阿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值