微信小程序日程预约

server/2025/2/13 16:10:28/

涉及仪器的预约使用,仿照小米日历日程预约开发开发对应页。

效果展示

在这里插入图片描述

文章目录

  • 效果展示
  • 需求分析
  • 代码实现
  • 一、构建基础页面结构
    • 1. 顶部日期选择器
    • 2. 中部canvas绘制
    • 3. 底部数据回显
  • 二、中间canvas功能细分
    • 1. 激活状态的判断
    • 2. 时间块拉伸逻辑
    • 3. 时间块拖动逻辑
  • 三、底部数据回显
  • 总结


需求分析

  • 顶部七日选择器
    • 横向显示从当前日期开始后的七天,并区分月-日
    • 七天共计预约时间段综合为3
  • 中部canvas绘制区
    • 左侧时间刻度
    • 右侧绘制区,总计24格,每大格为1h,一大格后期拆分四小格,为15min
    • 右侧绘制区功能
      • 激活:单击
      • 长按:拖动激活区域移动选区,存在激活区域之间的互斥
      • 拉伸:双击后改变预约起止时间
  • 底部数据回显区
    • 显示预约时间段
    • 支持删除

代码实现

一、构建基础页面结构

在这里插入图片描述

1. 顶部日期选择器

获取当前日期,即六天后的所有日期,并解析出具体月-日,存入数组dateList

 // 初始化日期列表initDateList() {const dateList = [];const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];for (let i = 0; i < 7; i++) {const date = new Date();// 获取未来几天的日期date.setDate(date.getDate() + i);dateList.push({date: date.getTime(),month: date.getMonth() + 1,day: date.getDate(),weekDay: weekDays[date.getDay()]});}this.setData({ dateList });},
<view wx:for="{{ dateList }}" wx:key="date"class="date-item {{ currentDateIndex === index ? 'active' : '' }}"bindtap="onDateSelect"data-index="{{ index }}"><text class="date-text">{{ item.month }}-{{ item.day }}</text><text class="week-text">{{ item.weekDay }}</text><text class="today-text" wx:if="{{ index === 0 }}">今天</text></view>

canvas_65">2. 中部canvas绘制

左侧25条数据,从0:00-24:00,只作为标志数据;【主体】右侧24格,通过canvas进行绘制。

  1. 初始化canvas,获取宽高,并通过ctx.scale(dpr,dpr)缩放canvas适应设备像素比;
  2. 绘制网格
   for (let i = 0; i <= 24; i++) {ctx.beginPath();const y = i * hourHeight;ctx.moveTo(0, y);ctx.lineTo(width, y);ctx.stroke();}

3. 底部数据回显

canvas_81">二、中间canvas功能细分

1. 激活状态的判断

  1. 首先给canvas添加点击事件bindtouchstart="onCanvasClick"

获取点击坐标,并解析首次触摸点的位置touch[0]clientX clientY 是触摸点在屏幕上的坐标

const query = wx.createSelectorQuery();
query.select('#timeGridCanvas').boundingClientRect(rect => {const x = e.touches[0].clientX - rect.left;const y = e.touches[0].clientY - rect.top;
  1. 计算时间格
const hourIndex = Math.floor(y / this.data.hourHeight);

hourHeight: rect.height / 24,来自于initCanvas初始化时,提前计算好的每个时间格的高度

  1. 获取选中的时间段
const existingBlockIndex = this.data.selectedBlocks.findIndex(block => hourIndex >= block.startHour && hourIndex < block.endHour);

使用 findIndex 查找点击位置是否在已选时间段内

  1. 取消选中逻辑
if (existingBlockIndex !== -1) {// 从当前日期的选中块中移除const newSelectedBlocks = [...this.data.selectedBlocks];newSelectedBlocks.splice(existingBlockIndex, 1);// 从所有选中块中移除const currentDate = `${this.data.dateList[this.data.currentDateIndex].month}-${this.data.dateList[this.data.currentDateIndex].day}`;const allBlockIndex = this.data.allSelectedBlocks.findIndex(block => block.date === currentDate && block.startHour === this.data.selectedBlocks[existingBlockIndex].startHour);const newAllBlocks = [...this.data.allSelectedBlocks];if (allBlockIndex !== -1) {newAllBlocks.splice(allBlockIndex, 1);}this.setData({selectedBlocks: newSelectedBlocks,allSelectedBlocks: newAllBlocks});
}

同时需要考虑两个数组:当前日期选中时间段selectedBlocks,七日内选中时间段总数allSelectedBlocks

  1. 新增时间段逻辑
else {// 检查限制if (this.data.allSelectedBlocks.length >= 3) {wx.showToast({title: '最多只能选择3个时间段',icon: 'none'});return;}// 添加新时间段const startHour = Math.floor(y / this.data.hourHeight);const endHour = startHour + 1;const newBlock = {date: `${this.data.dateList[this.data.currentDateIndex].month}-${this.data.dateList[this.data.currentDateIndex].day}`,startHour: startHour,endHour: endHour,startTime: this.formatTime(startHour * 60),endTime: this.formatTime(endHour * 60)};this.setData({selectedBlocks: [...this.data.selectedBlocks, newBlock],allSelectedBlocks: [...this.data.allSelectedBlocks, newBlock]});
}

先检查是否达到最大选择限制,创建新的时间段对象

date: 当前选中的日期
startHour: 开始小时
endHour: 结束小时
startTime: 格式化后的开始时间
endTime: 格式化后的结束时间

2. 时间块拉伸逻辑

  1. 检测拉伸手柄
    为了避免和后期的长按拖动逻辑的冲突,在选中时间块上额外添加上下手柄以作区分:
checkResizeHandle(x, y) {const handleSize = 16; // 手柄的点击范围大小for (let i = 0; i < this.data.selectedBlocks.length; i++) {const block = this.data.selectedBlocks[i];const startY = block.startHour * this.data.hourHeight;const endY = block.endHour * this.data.hourHeight;// 检查是否点击到上方手柄if (y >= startY - handleSize && y <= startY + handleSize) {return { blockIndex: i, isStart: true, position: startY };}// 检查是否点击到下方手柄if (y >= endY - handleSize && y <= endY + handleSize) {return { blockIndex: i, isStart: false, position: endY };}}return null;
}
  1. 处理拖拽拉伸逻辑
    在判断确定点击到拉伸手柄的情况下,处理逻辑
const resizeHandle = this.checkResizeHandle(x, y);if (resizeHandle) {// 开始拉伸操作this.setData({isResizing: true,resizingBlockIndex: resizeHandle.blockIndex,startY: y,initialY: resizeHandle.position,isResizingStart: resizeHandle.isStart});return;}
isResizing:标记正在拉伸
startY:开始拖动的位置
initialY:手柄的初始位置
isResizingStart:是否在调整开始时间
  1. 处理拖动过程
    需要根据拖动的距离来计算新的时间,将拖动的距离转换成时间的变化。简单来说,假设一小时占60px的高度,那么15min=15px,如果用户往下拖动30px,换算成时间就是30min。
// 计算拖动了多远
const deltaY = currentY - startY;  // 比如拖动了30像素// 计算15分钟对应的高度
const quarterHeight = hourHeight / 4;  // 假设hourHeight是60,那么这里是15// 计算移动了多少个15分钟
const quarterMoved = Math.floor(Math.abs(deltaY) / quarterHeight) * (deltaY > 0 ? 1 : -1);// 计算新的时间
const newTime = originalTime + (quarterMoved * 0.25);  // 0.25代表15分钟
  1. 更新时间显示
    计算出新的时间后,需要在确保有效范围内的同时,对齐15min的刻度并转化显示格式
// 确保时间合理,比如不能小于0点,不能超过24点
if (newTime >= 0 && newTime <= 24) {// 对齐到15分钟const alignedTime = Math.floor(newTime * 4) / 4;// 转换成"HH:MM"格式const hours = Math.floor(alignedTime);const minutes = Math.round((alignedTime - hours) * 60);const timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
  1. 结束拉伸逻辑
    当松手时,清楚拖动状态,将标识符置false
    this.setData({
    isResizing: false, // 结束拖动状态
    resizingBlockIndex: null, // 清除正在拖动的时间块
    startY: 0 // 重置起始位置
    });

3. 时间块拖动逻辑

  1. 长按时间块
    首先找到点击的时间块并存储信息,在原视图上”删除“该时间块,并标记拖动状态
onCanvasLongPress(e) {// 1. 先找到用户点击的是哪个时间块const hourIndex = Math.floor(y / this.data.hourHeight);const pressedBlockIndex = this.data.selectedBlocks.findIndex(block => hourIndex >= block.startHour && hourIndex < block.endHour);// 2. 如果真的点到了时间块if (pressedBlockIndex !== -1) {// 3. 保存这个时间块的信息,因为待会要用const pressedBlock = {...this.data.selectedBlocks[pressedBlockIndex]};// 4. 从原来的位置删除这个时间块const newBlocks = [...this.data.selectedBlocks];newBlocks.splice(pressedBlockIndex, 1);// 5. 设置拖动状态this.setData({isDragging: true,                // 标记正在拖动dragBlock: pressedBlock,         // 保存被拖动的时间块dragStartY: y,                   // 记录开始拖动的位置selectedBlocks: newBlocks,       // 更新剩下的时间块dragBlockDuration: pressedBlock.endHour - pressedBlock.startHour  // 记录时间块长度});}
}
  1. 时间块投影
    为了区分正常激活时间块,将长按的以投影虚化方式显示,提示拖动结束的位置。
    首先计算触摸移动的距离,并根据上文,推测相应时间变化。在合理的范围内,检测是否和其他时间块互斥,最终更新时间块的显示。
onCanvasMove(e) {if (this.data.isDragging) {const y = e.touches[0].clientY - rect.top;const deltaY = y - this.data.dragStartY;const quarterHeight = this.data.hourHeight / 4;const quarterMoved = Math.floor(deltaY / quarterHeight);const targetHour = this.data.dragBlock.startHour + (quarterMoved * 0.25);const boundedHour = Math.max(0, Math.min(24 - this.data.dragBlockDuration, targetHour));const isOccupied = this.checkTimeConflict(boundedHour, boundedHour + this.data.dragBlockDuration);this.setData({dragShadowHour: boundedHour,     // 投影的位置dragShadowWarning: isOccupied    // 是否显示冲突警告});}
}
  1. 互斥检测
    排除掉当前拖动时间块,检测与其余是否重叠。
    具体来说,假设当前时间块9:00-10:00,新位置9:30-10:30,这种情况 startHour(9:30) < block.endHour(10:00)endHour(10:30) > block.startHour(9:00)所以检测为重叠
checkTimeConflict(startHour, endHour) {return this.data.selectedBlocks.some(block => {if (block === this.data.dragBlock) return false;return (startHour < block.endHour && endHour > block.startHour);});
}
  1. 结束拖动
    当位置不互斥,区域有效的情况下,放置新的时间块,并添加到列表中,最后清理所有拖动相关的状态
onCanvasEnd(e) {if (this.data.isDragging) {if (this.data.dragShadowHour !== null && this.data.dragBlock && !this.data.dragShadowWarning) {const newHour = Math.floor(this.data.dragShadowHour * 4) / 4;const duration = this.data.dragBlockDuration;const newBlock = {startHour: newHour,endHour: newHour + duration,startTime: this.formatTime(Math.round(newHour * 60)),endTime: this.formatTime(Math.round((newHour + duration) * 60))};const newSelectedBlocks = [...this.data.selectedBlocks, newBlock];this.setData({ selectedBlocks: newSelectedBlocks });} else if (this.data.dragShadowWarning) {const newSelectedBlocks = [...this.data.selectedBlocks, this.data.dragBlock];this.setData({ selectedBlocks: newSelectedBlocks });wx.showToast({title: '该时间段已被占用',icon: 'none'});}this.setData({isDragging: false,dragBlock: null,dragStartY: 0,dragCurrentY: 0,dragShadowHour: null,dragBlockDuration: null,dragShadowWarning: false});}
}

三、底部数据回显

就是基本的数据更新回显,setData

  1. 新增时间段回显
const newBlock = {date: `${this.data.dateList[this.data.currentDateIndex].month}-${this.data.dateList[this.data.currentDateIndex].day}`,startHour: startHour,endHour: endHour,startTime: this.formatTime(startHour * 60),endTime: this.formatTime(endHour * 60)
};this.setData({allSelectedBlocks: [...this.data.allSelectedBlocks, newBlock]  
});
  1. 删除时间段映射
removeTimeBlock(e) {const index = e.currentTarget.dataset.index;const removedBlock = this.data.allSelectedBlocks[index];// 从总列表中删除const newAllBlocks = [...this.data.allSelectedBlocks];newAllBlocks.splice(index, 1);const currentDate = `${this.data.dateList[this.data.currentDateIndex].month}-${this.data.dateList[this.data.currentDateIndex].day}`;if (removedBlock.date === currentDate) {const newSelectedBlocks = this.data.selectedBlocks.filter(block => block.startHour !== removedBlock.startHour || block.endHour !== removedBlock.endHour);this.setData({ selectedBlocks: newSelectedBlocks });}this.setData({ allSelectedBlocks: newAllBlocks });
}

总结

相比于初版的div控制时间块的操作,canvas的渲染性能更好,交互也也更加灵活(dom操作的时候还需要考虑到阻止事件冒泡等情况),特别是频繁更新时,并且具有完全自定义的绘制能力和更精确的触摸事件处理。


http://www.ppmy.cn/server/167368.html

相关文章

如何使用deepseek等AI工具辅助web后端工作的开发

使用DeepSeek等AI工具辅助Web后端开发可以显著提升效率,以下是具体应用场景和操作指南: 一、核心开发场景 代码生成与补全示例场景:快速生成CRUD接口 操作:输入提示词 用Node.js Express框架编写用户管理模块,要求: - RESTful API设计 - 包含创建/查询/更新/删除接口 - …

微服务..

Spring Cloud 1. 什么是Spring Cloud&#xff1f; 答案&#xff1a; Spring Cloud是一个基于Spring Boot的开源框架&#xff0c;用于构建分布式系统。它提供了一系列的工具和库&#xff0c;用于简化微服务架构的开发和部署。Spring Cloud的核心功能包括服务注册与发现、配置管…

常用HAL库

宏定义 #define LED1_PIN GPIO_PIN_12 //定义引脚别名 #define LED1_GPIO_PORT GPIOB // GPIO_PIN_12 GPIOB确定指定引脚 #define LED1_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() //定义方法别名 #define LED1(a) HAL_GPIO_WritePin(LED1_GPIO_PORT,LED1_P…

网络安全设备异构要求 网络安全设备硬件

导航目录&#xff1a; 一、网络的设备 1. 网络传输介质互联设备2. 物理层互联设备3. 数据链路层互联设备4. 网络层互联设备5. 应用层互联设备 二、网络的传输介质 1. 有线介质2. 无线介质 三、组建网络 一、网络的设备 1. 网络传输介质互联设备 网络传输介质互联设备包括…

declare和less

declare -x LESSCLOSE"/usr/bin/lesspipe %s %s" declare -x LESSOPEN"| /usr/bin/lesspipe %s" declare 是一个在 **Unix/Linux Shell**&#xff08;如 Bash&#xff09;中用于声明变量及其属性的命令。它通常用于设置变量的值、类型以及一些特殊属性&a…

从MySQL优化到脑力健康:技术人与效率的双重提升

文章目录 零&#xff1a;前言一&#xff1a;MySQL性能优化的核心知识点1. 索引优化的最佳实践实战案例&#xff1a; 2. 高并发事务的处理机制实战案例&#xff1a; 3. 查询性能调优实战案例&#xff1a; 4. 缓存与连接池的优化实战案例&#xff1a; 二&#xff1a;技术工作者的…

常见的排序算法:插入排序、选择排序、冒泡排序、快速排序

1、插入排序 步骤&#xff1a; 1.从第一个元素开始&#xff0c;该元素可以认为已经被排序 2.取下一个元素tem&#xff0c;从已排序的元素序列从后往前扫描 3.如果该元素大于tem&#xff0c;则将该元素移到下一位 4.重复步骤3&#xff0c;直到找到已排序元素中小于等于tem的元素…

【大数据安全分析】安全告警关联相关安全分析场景

一、引言 在当今数字化高度发展的时代,网络安全面临着前所未有的挑战。随着网络攻击手段的日益复杂和多样化,单一的安全告警往往难以全面、准确地反映网络安全态势。安全告警关联分析作为一种有效的安全分析方法,通过对多个安全告警进行关联和整合,能够发现潜在的攻击模式…