动态添加时间范围,如何置灰已选择时间?

先说下需求:开始时段和结束时段是24小时按照30分钟分割成48个点的数据,数据格式如下:["00:00","00:30","01:00","01:30",.....,"23:30"],每行数据后都有新增和删除操作。

1、开始时段选择后,结束时段小于开始时段的值置灰不能选择。

2、假如:开始时段选择了 "01:00",结束时段选择了 "03:30",点击后边新增时,前边已选的数据置灰不能选择,第二条的数据选择只能在这个范围内:"00:00"-"01:00","03:30"-"23:30",
当用户开始时间选择了"00:00",则结束时间只能选择"00:00"到"01:00"范围内的任何一个值,其他时间段都置灰。
当用户开始时间选择了"03:30",则结束时间只能在"03:30"到"23:30"范围内的任何一个值,而"00:00"到"03:30"置灰不能选择。

3、当第二条数据选择了 "05:00",结束时间选择了"05:30",点击后边新增时,前边已选的数据置灰不能选择,第三条的数据选择只能在这个范围内:"00:00"-"01:00","03:30"-"05:00","05:30"-"23:00",
当用户开始时间选择了"00:00",则结束时间只能选择"00:00"到"01:00"范围内的任何一个值,其他时间段都置灰。
当用户开始时间选择了"03:30",则结束时间只能选择"03:30"到"05:00"范围内的任何一个值,其他时间段都置灰。
当用户开始时间选择了"05:30",则结束时间只能在"05:30"到"23:30"范围内的任何一个值,而"00:00"到"05:30"置灰不能选择。
以此类推...

4、当删除某行数据时,已删除的数据要重新进行可选和置灰操作。

页面如下:
初次进入页面显示:
image.png
添加一行数据显示:
image.png

以下是我的代码,各位大佬后边的逻辑如何实现?

https://codepen.io/wdvkmqph-the-reactor/pen/abxVWrK

阅读 1.5k
6 个回答

父组件代码

<template>
  <div class="app-container">
    <el-table
      size="mini"
      class="mt15"
      border
      :data="tableData"
      :span-method="arraySpanMethod"
    >
      <el-table-column
        label="季节"
        prop="season"
        align="center"
        min-width="120"
      >
      </el-table-column>
      <el-table-column
        label="时段"
        prop="period"
        align="center"
        min-width="120"
      >
      </el-table-column>
      <el-table-column
        label="具体时间"
        prop="timeList"
        align="center"
        min-width="350"
      >
        <template slot-scope="scope">
          <span
            v-for="(item, index) in scope.row.timeList"
            :key="index"
            class="ml8"
          >
            <el-tag size="mini" v-if="item.startTime">{{
              item.startTime + "-" + item.endTime
            }}</el-tag>
          </span>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" min-width="120">
        <template slot-scope="scope">
          <el-button
            type="text"
            class="primary"
            size="mini"
            icon="el-icon-edit"
            @click="handleFormEdit(scope.row, scope.$index)"
            >编辑
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 添加时间弹窗 -->
    <add-time
      :show-add-dialog="showAddDialog"
      @get-add-result="getAddResult"
    ></add-time>
  </div>
</template>
<script>
// 组件
import AddTime from "./component/addTime.vue";
// utils
import { deepClone } from "js-fastcode";
export default {
  name: "VueTemplateIndex",
  components: { AddTime },
  data() {
    return {
      companyArr: [],
      companyPos: 0,
      showAddDialog: { visible: false },
      idx: "", // 编辑当前行行数
      tableData: [],
    };
  },
  mounted() {
    const season = ["春季", "夏季"];
    const period = ["尖峰", "高峰",  "低谷"];
    this.tableData = Array.from({ length: season.length }, (_, i) =>
      period.map((pj, j) => ({
        season: season[i],
        period: pj,
        timeList: [],
      }))
    ).flat();
    this.merge(this.tableData);
  },
  methods: {
    // 表格行合并方法
    merge(tableData) {
      // 要合并的数组的方法
      this.companyArr = [];
      this.companyPos = 0;
      for (let i = 0; i < tableData.length; i++) {
        if (i === 0) {
          // 第一行必须存在
          this.companyArr.push(1);
          this.companyPos = 0;
        } else {
          // 判断当前元素与上一个元素是否相同 this.companyPos是companyArr内容的序号
          if (tableData[i].season === tableData[i - 1].season) {
            this.companyArr[this.companyPos] += 1;
            this.companyArr.push(0);
          } else {
            this.companyArr.push(1);
            this.companyPos = i;
          }
        }
      }
    },
    // 合并行
    arraySpanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex === 0) {
        const _row_1 = this.companyArr[rowIndex];
        const _col_1 = _row_1 > 0 ? 1 : 0; // 如果被合并了_row=0则它这个列需要取消
        return {
          rowspan: _row_1,
          colspan: _col_1,
        };
      }
    },
    // 新增数据
    handleFormEdit(list, index) {
      let arr = deepClone(this.tableData),
        brr = [];
      brr = arr
        .filter((item) => item.season === list.season)
        .map((item) => item.timeList)
        .flat();
      this.idx = index;
      this.showAddDialog = {
        visible: true,
        title: "编辑",
        data: brr,
        list: list.timeList,
      };
    },
    // 新增回调
    getAddResult(list) {
      this.tableData[this.idx].timeList = list;
    },
  },
};
</script>

子组件代码

<template>
  <el-dialog
    :visible.sync="showAddDialog.visible"
    v-dialogDrag
    width="620px"
    :title="showAddDialog.title"
    :close-on-click-modal="false"
    :append-to-body="true"
    @close="handleDialogClose"
  >
    <el-table size="mini" border :data="tableData">
      <el-table-column prop="startTime" align="center" label="开始时段">
        <template slot-scope="scope">
          <el-select
            v-model="scope.row.startTime"
            style="width: 138px"
            placeholder="开始时段"
            size="mini"
            :disabled="scope.row.disabled"
            @change="handleStartChange(scope.row)"
          >
            <el-option
              v-for="item in startTimeList"
              :key="item.value"
              :label="item.label"
              :value="item.value"
              :disabled="item.disabled"
            >
            </el-option>
          </el-select>
        </template>
      </el-table-column>
      <el-table-column prop="endTime" align="center" label="结束时段">
        <template slot-scope="scope">
          <el-select
            v-model="scope.row.endTime"
            style="width: 138px"
            placeholder="结束时段"
            size="mini"
            :disabled="!scope.row.startTime || scope.row.disabled"
          >
            <el-option
              v-for="item in endTimeList"
              :key="item.value"
              :label="item.label"
              :value="item.value"
              :disabled="item.disabled"
            >
            </el-option>
          </el-select>
        </template>
      </el-table-column>
      <el-table-column align="center" label="操作">
        <template slot-scope="scope">
          <span>
            <el-button
              v-if="scope.$index === tableData.length - 1"
              type="primary"
              icon="el-icon-plus"
              circle
              size="mini"
              @click="handleRowAdd(scope.row, scope.$index)"
            >
            </el-button>
            <el-button
              type="warning"
              icon="el-icon-minus"
              circle
              size="mini"
              @click="handleRowDelete(scope.row, scope.$index)"
            ></el-button>
          </span>
        </template>
      </el-table-column>
    </el-table>
    <div slot="footer" class="dialog-footer">
      <el-button size="small" @click="handleDialogClose">取 消</el-button>
      <el-button size="small" type="primary" @click="handleDialogSave">
        确 定
      </el-button>
    </div>
  </el-dialog>
</template>

<script>
import { devideTimes, deepClone } from "js-fastcode"; // 引入自定义js库
export default {
  name: "",
  props: {
    showAddDialog: {
      type: Object,
      default: () => ({}),
    },
  },
  data() {
    return {
      tableData: [],
      propsData: [], // 存储父组件传递过来的时间段
      allAddData: [], // 存储所有已选时间段
    };
  },
  watch: {
    showAddDialog: {
      handler(newVal, oldVal) {
        this.tableData = [];
        // 如果当前行有数据,则显示当前行数据
        if (newVal.list.length) {
          this.tableData = newVal.list.map((item) => ({
            ...item,
            disabled: true,
          }));
        } else {
          // 如果当前行没有数据,则默认显示开始时间和结束时间
          this.tableData = [{ startTime: "", endTime: "" }];
        }
        for (let i in this.endTimeList) {
          this.endTimeList[i].disabled = false;
        }
        for (let i in this.startTimeList) {
          this.startTimeList[i].disabled = false;
        }
        this.propsData = deepClone(newVal.data);
        // 将当前季节下的所有时间段都放在数组中
        this.allAddData = deepClone(newVal.data);
        this.handleDisable();
      },
      deep: true,
    },
  },
  computed: {
    // 获取默认的1-24小时数据
    timeOptions() {
      return devideTimes(30, 2);
    },
    startTimeList() {
      return this.timeOptions.map((item) => ({
        value: item,
        label: item,
        disabled: false,
      }));
    },
    endTimeList() {
      return this.timeOptions.map((item) => ({
        value: item,
        label: item,
        disabled: false,
      }));
    },
  },
  mounted() {},
  methods: {
    // 新增行
    handleRowAdd(row, idx) {
      this.allAddData = this.handleUnique([
        ...this.tableData,
        ...this.propsData,
      ]);
      this.handleDisable();
      // 判断开始时间所有项全部是不是禁用状态,如果是则说明所有时段已选择,否则未全部选择
      let flag = this.startTimeList.every((item) => item.disabled);

      if (flag) {
        this.$message.warning("所有时段都已选择");
        return false;
      }
      if (!row.startTime && !row.endTime) {
        this.$message.warning("开始时段和结束时段必填");
        return false;
      }
      this.tableData[idx].disabled = true;
      this.tableData.push({
        startTime: "",
        endTime: "",
      });
    },
    // 删除行
    handleRowDelete(row, index) {
      // 当只有一条数据时,初始化表格数据
      if (this.tableData.length === 1) {
        this.tableData = [{ startTime: "", endTime: "" }];
      } else {
        this.tableData.splice(index, 1);
      }

      this.propsData = this.propsData.filter(
        (item) =>
          !(item.startTime === row.startTime && item.endTime === row.endTime)
      );
      this.allAddData = this.handleUnique([
        ...this.tableData,
        ...this.propsData,
      ]);
      this.handleDisable();
    },
    // 数组对象去重
    handleUnique(arr) {
      let obj = {};
      return arr.reduce((cur, next) => {
        obj[next.startTime + next.endTime]
          ? ""
          : (obj[next.startTime + next.endTime] = true && cur.push(next));
        return cur;
      }, []);
    },
    // 切换起始时间
    handleStartChange(time) {
      let times = this.timeOptions;
      // 找到所选开始时间的下标
      let start_index = times.findIndex((value) => value === time.startTime);
      // 将结束时间小于开始时间的选项禁用
      for (let i = 0; i < start_index; i++) {
        this.endTimeList[i].disabled = true;
      }
      // 根据开始时间和结束时间的可用性设置结束时间的禁用状态
      for (let i = start_index + 1; i < this.startTimeList.length; i++) {
        if (this.startTimeList[i].disabled) {
          for (const [index, endTime] of this.endTimeList.entries()) {
            if (start_index <= index && index < i) {
              endTime.disabled = false;
            } else {
              endTime.disabled = true;
            }
          }
          break;
        } else {
          this.endTimeList[i].disabled = false;
        }
      }
    },
    // 禁止时间交集
    handleDisable() {
      let times = this.timeOptions;
      // 将开始时段所有选项置为可用
      this.startTimeList.forEach((start) => {
        start.disabled = false;
      });

      // 遍历所有已选时间段
      this.allAddData.forEach((item) => {
        let start_index = times.findIndex((value) => value === item.startTime);
        let end_index = times.findIndex((value) => value === item.endTime);
        this.startTimeList.forEach((start, i) => {
          if (i >= start_index && i <= end_index) {
            start.disabled = true;
          }
        });
      });
    },
    handleDialogClose() {
      this.showAddDialog.visible = false;
    },
    // 保存
    handleDialogSave() {
      // 当前数据大于两条,但是有一条数据未填写进行提示判断
      let flag =
        this.tableData.length >= 2 &&
        this.tableData.some(
          (item) => item.startTime === "" || item.endTime === ""
        );
      if (flag) {
        this.$message.warning("请将所有时段填写完整");
        return false;
      }
      // 提交时将未填写时段的数据去除,主要是针对只有一条数据未填写情况
      this.tableData = this.tableData.filter(
        (item) => item.startTime && item.endTime
      );
      this.showAddDialog.visible = false;
      this.$emit("get-add-result", this.tableData);
    },
  },
};
</script>

<style scoped lang="scss">
.remove-icon,
.plus-icon {
  font-size: $fs22;
  vertical-align: middle;
  margin-left: 10px;
}
</style>

试试这个

// 计算结束时间是否应被禁用
isDisabledEndTime(timeOption, dataIndex) {
  if (dataIndex === 0) {
    // 第一行的结束时间不受限制
    return false;
  }

  const previousEnd = this.allTimeData[dataIndex - 1].endTime;

  // 当前选择的开始时间
  const currentStart = this.allTimeData[dataIndex].startTime;
  // 遍历已占用的时间区间,找到与当前开始时间相邻的占用区间
  const occupiedInterval = this.occupiedIntervals.find(
    interval => currentStart >= interval[0] && currentStart <= interval[1]
  );

  if (occupiedInterval) {
    // 如果找到与当前开始时间相邻的占用区间,则禁用在该区间内的结束时间
    return timeOption >= occupiedInterval[0] && timeOption <= occupiedInterval[1];
  } else {
    // 在没有找到相邻占用区间的情况下,仅当结束时间小于等于前一行结束时间时禁用
    return timeOption <= previousEnd;
  }
},
// 更新禁用状态的方法
updateOccupiedIntervals() {
  this.occupiedIntervals = [];
  for (let i = 0; i < this.allTimeData.length; i++) {
    const start = this.timeOptions.indexOf(this.allTimeData[i].startTime);
    const end = this.timeOptions.indexOf(this.allTimeData[i].endTime);

    // 将每个有效时间段添加到占用区间列表,但排除与前一时间段相连的情况
    if (i === 0 || start > this.occupiedIntervals[this.occupiedIntervals.length - 1][1]) {
      this.occupiedIntervals.push([start, end]);
    } else {
      this.occupiedIntervals[this.occupiedIntervals.length - 1][1] = Math.max(this.occupiedIntervals[this.occupiedIntervals.length - 1][1], end);
    }
  }
},

我觉得你可以动态生成 timeOptions
你可以用一个计算属性调用 generatorTimeOptions ,根据已选的数据传入适当的 skipList 这样你就不用关心 删除 新增这些逻辑了。

/**
 * 生成时间选项
 *
 * @param startTime 开始时间,单位为分钟,默认为0
 * @param endTime 结束时间,单位为分钟,默认为1440
 * @param step 步长,单位为分钟,默认为30
 * @param skipList 需要跳过的时间段列表,格式为[['00:00', '01:00'], '02:00'],默认为空数组
 * @returns 返回时间选项数组,格式为['00:00', '00:30', ...]
 */
export function generatorTimeOptions(startTime = 0, endTime = 1440, step = 30, skipList = []) {
  let currentTime = startTime
  const options = []
  let skipMinutes = []
  if (skipList && skipList.length > 0) {
    skipMinutes = skipList.map((item) => {
      if (Array.isArray(item) && item.length === 2) {
        return {
          start: timeToMinutes(item[0]),
          end: timeToMinutes(item[1]),
        }
      } else if (Array.isArray(item) && item.length === 1) {
        return {
          start: timeToMinutes('00:00'),
          end: timeToMinutes(item[0]),
        }
      } else if (item && item.split(':').length === 2) {
        return {
          start: timeToMinutes('00:00'),
          end: timeToMinutes(item),
        }
      }
    })
  }
  while (currentTime < endTime) {
    let skip = false
    if (skipMinutes.length > 0) {
      skipMinutes.forEach((item) => {
        if (currentTime >= item.start && currentTime <= item.end) {
          skip = true
        }
      })
    }
    if (!skip) {
      options.push(minutesToTime(currentTime))
    }
    currentTime += step
  }
  return options
}
function timeToMinutes(time = '00:00') {
  return time.split(':').reduce((total, cur, index) => {
    if (index === 0) {
      total += Number(cur) * 60
    } else {
      total += Number(cur)
    }
    return total
  }, 0)
}
function minutesToTime(minutes = 0) {
  let hour = Math.floor(minutes / 60)
  let minute = minutes % 60
  if (minute < 10) {
    minute = `0${minute}`
  }
  if (hour < 10) {
    hour = `0${hour}`
  }
  return `${hour}:${minute}`
}

上面的代码你可以存为一个文件
然后在你的代码里

import {generatorTimeOptions} from '文件位置'
// ...
computed: {
   timeOptions() {
      const skipList = this.allTimeData.map(item => {
         return item.startTime && item.endTime ? {
            start: item.startTime,
            end: item.endTime,
         } : item.startTime
      })
      return generatorTimeOptions(0, 1440, 30, skipList);
   }

我目前没有环境,只能讲个思路

既然需要处理选项的状态(禁用),那一开始的时间选项就可以处理成带状态的数据,顺便也处理成显示所需要的结构:

const timeOptions = ["00:00", ..., "23:30"]
    .map(it => ({ value: it, label: it, disabled: false }))

然后每次选中了某个时间段,就在 timeOptions 里找到需要禁用的选择,改 disabled 值为 true 即可。

补充需要注意的一个问题,当需要修改某个选项的时候,由于这个选项之前选的时间段也需要可选择,可以理解为之前选过的时间段放回可选项池,所以这时候需要把这部分选项重新标记为 disabled: false

如果再深入一点考虑,选择的时候用高亮色来表示当前选择但未确定的时间段,那就可以再加一个状态来处理(或者将 disabled 改为一个可以用三种状态值表示的状态)。

新手上路,请多包涵

当第二条数据选择了 "05:00",结束时间选择了"05:30",点击后边新增时,前边已选的数据置灰不能选择,第三条的数据选择只能在这个sdasdada

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题