当前位置: 首页 > article >正文

微信小程序日程预约

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

效果展示

在这里插入图片描述

文章目录

  • 效果展示
  • 需求分析
  • 代码实现
  • 一、构建基础页面结构
    • 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>

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功能细分

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.kler.cn/a/546798.html

相关文章:

  • 【Python深入浅出㊵】解锁Python3的requests模块:网络请求的魔法钥匙
  • 遵循规则:利用大语言模型进行视频异常检测的推理
  • DeepSeek v3 技术报告阅读笔记
  • spring 中 AspectJ 基于 XML 的实现分析
  • 安全启动(secure boot)怎么关闭_史上最全的各品牌机和组装机关闭安全启动教程
  • 将错误消息输出到标准错误流:Rust中的最佳实践
  • web第三次作业
  • 浏览器安全学习
  • 中兴R5300 G4服务器配置磁盘RAID
  • 人工智能之知识图谱实战系列
  • 三格电子——TCP转ProfibusDP网关使用场景
  • 从技术债务到架构升级,滴滴国际化外卖的变革
  • [0696].第11节:Kafka-Eagle监控
  • dayjs的isSameOrAfter、isSameOrBefore、isAfter、isBefore学习
  • 微软AutoGen高级功能——Selector Group Chat
  • 【webview Android】视频获取首帧为封面
  • 服务器防护(ubuntu)
  • 辛格迪客户案例 | 钥准医药科技GMP文件管理(DMS)项目
  • oracle 19c安装DBRU补丁时报错CheckSystemSpace的处理
  • 百度 AI开源!将在6月30日开源文心大模型4.5系列