效果展示
组件功能
- 按天选择:支持选择周一到周日共7天
- 时间段选择:每天的时间被划分为48个半小时的时间段
- 可视化展示:使用表格直观展示时间段选择状态
- 数据绑定:通过
v-model
实现双向数据绑定 - 禁用状态:支持整体禁用功能
组件结构
-
模板部分:
- 星期选择标签页:顶部显示7天,可点击切换
- 时间表格:分为上午和下午两部分,每半小时一个单元格
- 交互设计:选中的时间段会高亮显示,禁用状态有特殊样式
-
脚本部分:
- props:接收
value
(已选时间段数据)和disabled
(禁用状态) - data:维护当前选中的日期和所有时间槽
- computed:格式化已选时间段数据
- methods:包含日期切换、时间选择等核心逻辑
- props:接收
-
样式部分:
- 使用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分钟拆分,不支持自定义时间段。等…
结语
半吊子再半吊子前端,若各位有更好的建议或更新优化,欢迎评论分享。