Vue2 时间段选择组件

效果展示

无选择效果
选择效果1
选择效果2
禁用效果

组件功能

  • 按天选择:支持选择周一到周日共7天
  • 时间段选择:每天的时间被划分为48个半小时的时间段
  • 可视化展示:使用表格直观展示时间段选择状态
  • 数据绑定:通过 v-model 实现双向数据绑定
  • 禁用状态:支持整体禁用功能

组件结构

  • 模板部分

    • 星期选择标签页:顶部显示7天,可点击切换
    • 时间表格:分为上午和下午两部分,每半小时一个单元格
    • 交互设计:选中的时间段会高亮显示,禁用状态有特殊样式
  • 脚本部分

    • props:接收 value(已选时间段数据)和 disabled(禁用状态)
    • data:维护当前选中的日期和所有时间槽
    • computed:格式化已选时间段数据
    • methods:包含日期切换、时间选择等核心逻辑
  • 样式部分

    • 使用scoped样式确保样式只作用于当前组件
    • 提供了良好的视觉反馈,包括悬停效果和选中状态

使用方法

该组件使用 v-model 进行双向数据绑定,绑定的数据格式为:

[
  {
    day: 1, // 星期几(1-7)
    timeScopeRuleList: [
      {
        startTime: "08:00:00",
        endTime: "08:30:00"
      },
      {
        startTime: "09:00:00",
        endTime: "09:30:00"
      }
    ]
  }
]

在父组件中使用示例:

<template>
  <div>
    <TimeSlotSelector v-model="selectedTimes" />
    <div>已选择的时间段: {{ selectedTimes }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedTimes: []
    }
  }
}
</script>

组件会自动处理时间选择的逻辑,并通过事件将结果传递给父组件。

组件源代码

<template>
  <div class="time-slot-selector">
    <!-- 星期选择标签页 -->
    <div class="days-tabs">
      <el-badge
        v-for="day in 7"
        :value="daySelectNum(day)/2 + 'h'"
        class="item"
        type="primary"
        :key="day"
        :hidden="daySelectNum(day) == 0"
      >
        <div
          :key="day"
          :class="{ 'tab-item': true, active: currentDay === day }"
          @click="switchDay(day)"
        >
          {{ getDayName(day) }}
        </div>
      </el-badge>
    </div>

    <table class="time-table">
      <!-- 上午/下午 -->
      <tr>
        <th class="time-th" colspan="24">00:00 - 12:00</th>
        <th class="time-th" colspan="24">12:00 - 23:59</th>
      </tr>
      <!-- 小时 -->
      <tr>
        <td
          v-for="(time, index) in timeSlots.slice(0, Math.floor(timeSlots.length / 2))"
          colspan="2"
          :key="index"
          class="time-label"
        >
          {{ index }}
        </td>
      </tr>
      <!-- 30分钟 -->
      <tr>
        <td
          v-for="(time, index) in timeSlots"
          :key="index"
          :class="{
            'time-cell': true,
            selected: isTimeSelected(currentDay, time),
            disabled: disabled
          }"
          @click="!disabled && selecteTime(currentDay, index)"
        ></td>
      </tr>
    </table>
  </div>
</template>

<script>
export default {
  name: 'TimeSlotSelector',
  props: {
    value: {
      type: Array,
      default: () => [],
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      currentDay: 1,
      timeSlots: Array.from({ length: 48 }, (_, i) => {
        const hour = Math.floor(i / 2);
        const minute = (i % 2) * 30;
        return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:00`;
      }),
    };
  },
  computed: {
    formattedValue() {
      return this.value.map((dayItem) => ({
        ...dayItem,
        timeScopeRuleList: dayItem.timeScopeRuleList
          .sort((a, b) => a.startTime.localeCompare(b.startTime)),
      }));
    },
  },
  // 添加生命周期钩子,监听组件显示状态
  mounted() {
    // 组件首次挂载时(抽屉首次打开)重置选中项
    this.switchDay(1);
  },
  activated() {
    // 如果组件被缓存(如使用 <keep-alive>),重新激活时重置选中项
    this.switchDay(1);
  },
  methods: {
    // 获取星期名称
    getDayName(day) {
      const days = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'];
      return days[day - 1];
    },

    // 获取星期已选择时间段数量
    daySelectNum(day) {
      const dayData = this.formattedValue.find(
        (item) => item.day === day || item.day === String(day),
      );
      if (dayData) {
        return dayData.timeScopeRuleList.length;
      }
      return 0;
    },

    // 切换当前选择的日期
    switchDay(day) {
      this.currentDay = day;
    },

    // 检查时间是否已被选中
    isTimeSelected(day, time) {
      const dayData = this.formattedValue.find(
        (item) => item.day === day || item.day === String(day),
      );
      if (!dayData) return false;

      const endTime = this.endTime(this.timeSlots.indexOf(time));
      if (!endTime) return false;

      return dayData.timeScopeRuleList.some(
        (rule) => rule.startTime === time && rule.endTime === endTime,
      );
    },

    // 获取结束时间
    endTime(index) {
      const nextTimeIndex = index + 1;
      if (nextTimeIndex > this.timeSlots.length) {
        return null;
      }
      if (nextTimeIndex === this.timeSlots.length) {
        return '23:59:59';
      }
      return this.timeSlots[nextTimeIndex];
    },

    // 选择时间段
    selecteTime(day, index) {
      const selected = !this.isTimeSelected(day, this.timeSlots[index]);

      // 更新选中状态
      this.toggleTimeSlot(day, index, selected);
    },

    // 切换时间段选中状态
    toggleTimeSlot(day, index, selected) {
      const time = this.timeSlots[index];

      const endTime = this.endTime(index);
      if (!endTime) return;

      // 找到当前日期的数据
      const dayDataIndex = this.formattedValue.findIndex(
        (item) => item.day === day || item.day === String(day),
      );

      let finalValue = [];
      if (selected) {
        // 添加时间段
        const newRule = {
          startTime: time,
          endTime,
        };

        if (dayDataIndex === -1) {
          // 如果当前日期没有数据,创建新数据
          finalValue = [
            ...this.formattedValue,
            {
              day,
              timeScopeRuleList: [newRule],
            },
          ];
        } else {
          // 如果已有数据,添加新时间段
          const newDayData = {
            ...this.formattedValue[dayDataIndex],
            timeScopeRuleList: [...this.formattedValue[dayDataIndex].timeScopeRuleList, newRule],
          };

          finalValue = [
            ...this.formattedValue.slice(0, dayDataIndex),
            newDayData,
            ...this.formattedValue.slice(dayDataIndex + 1),
          ];
        }
      } else {
        // 移除时间段
        if (dayDataIndex === -1) return;

        const filteredRules = this.formattedValue[dayDataIndex].timeScopeRuleList.filter(
          (rule) => !(rule.startTime === time && rule.endTime === endTime),
        );

        if (filteredRules.length === 0) {
          // 如果移除后没有时间段了,删除整个日期数据
          finalValue = [
            ...this.formattedValue.slice(0, dayDataIndex),
            ...this.formattedValue.slice(dayDataIndex + 1),
          ];
        } else {
          // 更新日期数据
          const newDayData = {
            ...this.formattedValue[dayDataIndex],
            timeScopeRuleList: filteredRules,
          };

          finalValue = [
            ...this.formattedValue.slice(0, dayDataIndex),
            newDayData,
            ...this.formattedValue.slice(dayDataIndex + 1),
          ];
        }
      }
      this.$emit('input', finalValue);
      // 手动触发表单验证
      this.$parent.$emit('el.form.change', finalValue);
    },
  },
};
</script>

<style scoped>
.time-slot-selector {
  font-family: Arial, sans-serif;
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  padding: 16px;
}

.days-tabs {
  display: flex;
  margin-bottom: 16px;
  border-bottom: 1px solid #e0e0e0;
}

.tab-item {
  padding: 10px 16px;
  cursor: pointer;
  font-weight: 500;
  color: #666;
  transition: all 0.2s;
  border-bottom: 2px solid transparent;
}

.tab-item.active {
  color: #2c3e50;
  border-bottom-color: #3498db;
}

.time-table {
  border-collapse: collapse;
}

.time-th {
  height: 40px;
  border: 1px solid #e0e0e0;
  font-size: 15px;
  color: #666;
}

.time-label {
  height: 35px;
  font-size: 13px;
  color: #666;
  border: 1px solid #e0e0e0;
  text-align: center;
}

.time-cell {
  height: 35px;
  border: 1px solid #e0e0e0;
  margin: -1px 0 0 -1px;
  background-color: #f9f9f9;
  cursor: pointer;
  transition: background-color 0.2s;
  width: 20px;
}

.time-cell.selected {
  background-color: rgb(64, 158, 255);
  z-index: 1;
}

.time-cell.disabled {
  cursor: not-allowed;
}

.time-cell.selected.disabled {
  background-color: rgb(160, 207, 255);
}

.time-cell:not(.selected, .disabled):hover {
  background-color: rgb(230, 245, 255);
  z-index: 1;
}

.time-cell.selected:not(.disabled):hover {
  background-color: rgb(102, 177, 255);
  z-index: 1;
}

</style>

可优化点

  • 数据格式不通用:开发时按后端接收格式设计,个人认为改成 ["yyyy-MM-dd HH:mm:ss"]列表更通用,甚至可以合并时间短 [["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss"]]
  • 不支持滑动选择:只能单个单元格选择,若需选择时间多,用户体验不好。
  • 可配置性不高:固定按30分钟拆分,不支持自定义时间段。等…

结语

半吊子再半吊子前端,若各位有更好的建议或更新优化,欢迎评论分享。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值