微信小程序日程預約功能實現(xiàn)
涉及儀器的預約使用,仿照小米日歷日程預約開發(fā)開發(fā)對應頁。
效果展示
需求分析
- 頂部七日選擇器
- 橫向顯示從當前日期開始后的七天,并區(qū)分月-日
- 七天共計預約時間段綜合為3
- 中部canvas繪制區(qū)
- 左側(cè)時間刻度
- 右側(cè)繪制區(qū),總計24格,每大格為1h,一大格后期拆分四小格,為15min
- 右側(cè)繪制區(qū)功能
- 激活:單擊
- 長按:拖動激活區(qū)域移動選區(qū),存在激活區(qū)域之間的互斥
- 拉伸:雙擊后改變預約起止時間
- 底部數(shù)據(jù)回顯區(qū)
- 顯示預約時間段
- 支持刪除
代碼實現(xiàn)
一、構建基礎頁面結構
1. 頂部日期選擇器
獲取當前日期,即六天后的所有日期,并解析出具體月-日,存入數(shù)組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繪制
左側(cè)25條數(shù)據(jù),從0:00-24:00,只作為標志數(shù)據(jù);【主體】右側(cè)24格,通過canvas進行繪制。
初始化canvas,獲取寬高,并通過ctx.scale(dpr,dpr)
縮放canvas適應設備像素比;
繪制網(wǎng)格
for (let i = 0; i <= 24; i++) { ctx.beginPath(); const y = i * hourHeight; ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); }
3. 底部數(shù)據(jù)回顯
二、中間canvas功能細分
1. 激活狀態(tài)的判斷
首先給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;
計算時間格
const hourIndex = Math.floor(y / this.data.hourHeight);
hourHeight: rect.height / 24
,來自于initCanvas初始化時,提前計算好的每個時間格的高度
獲取選中的時間段
const existingBlockIndex = this.data.selectedBlocks.findIndex(block => hourIndex >= block.startHour && hourIndex < block.endHour );
使用 findIndex
查找點擊位置是否在已選時間段內(nèi)
取消選中邏輯
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 }); }
同時需要考慮兩個數(shù)組:當前日期選中時間段selectedBlocks
,七日內(nèi)選中時間段總數(shù)allSelectedBlocks
新增時間段邏輯
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] }); }
先檢查是否達到最大選擇限制,創(chuàng)建新的時間段對象
date: 當前選中的日期 startHour: 開始小時 endHour: 結束小時 startTime: 格式化后的開始時間 endTime: 格式化后的結束時間
2. 時間塊拉伸邏輯
檢測拉伸手柄
為了避免和后期的長按拖動邏輯的沖突,在選中時間塊上額外添加上下手柄以作區(qū)分:
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; }
處理拖拽拉伸邏輯
在判斷確定點擊到拉伸手柄的情況下,處理邏輯
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:是否在調(diào)整開始時間
處理拖動過程
需要根據(jù)拖動的距離來計算新的時間,將拖動的距離轉(zhuǎn)換成時間的變化。簡單來說,假設一小時占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分鐘
更新時間顯示
計算出新的時間后,需要在確保有效范圍內(nèi)的同時,對齊15min的刻度并轉(zhuǎn)化顯示格式
// 確保時間合理,比如不能小于0點,不能超過24點 if (newTime >= 0 && newTime <= 24) { // 對齊到15分鐘 const alignedTime = Math.floor(newTime * 4) / 4; // 轉(zhuǎn)換成"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')}`; }
結束拉伸邏輯
當松手時,清楚拖動狀態(tài),將標識符置false
this.setData({
isResizing: false, // 結束拖動狀態(tài)
resizingBlockIndex: null, // 清除正在拖動的時間塊
startY: 0 // 重置起始位置
});
3. 時間塊拖動邏輯
長按時間塊
首先找到點擊的時間塊并存儲信息,在原視圖上”刪除“該時間塊,并標記拖動狀態(tài)
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. 設置拖動狀態(tài) this.setData({ isDragging: true, // 標記正在拖動 dragBlock: pressedBlock, // 保存被拖動的時間塊 dragStartY: y, // 記錄開始拖動的位置 selectedBlocks: newBlocks, // 更新剩下的時間塊 dragBlockDuration: pressedBlock.endHour - pressedBlock.startHour // 記錄時間塊長度 }); } }
時間塊投影
為了區(qū)分正常激活時間塊,將長按的以投影虛化方式顯示,提示拖動結束的位置。
首先計算觸摸移動的距離,并根據(jù)上文,推測相應時間變化。在合理的范圍內(nèi),檢測是否和其他時間塊互斥,最終更新時間塊的顯示。
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 // 是否顯示沖突警告 }); } }
互斥檢測
排除掉當前拖動時間塊,檢測與其余是否重疊。
具體來說,假設當前時間塊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); }); }
結束拖動
當位置不互斥,區(qū)域有效的情況下,放置新的時間塊,并添加到列表中,最后清理所有拖動相關的狀態(tài)
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 }); } }
三、底部數(shù)據(jù)回顯
就是基本的數(shù)據(jù)更新回顯,setData
新增時間段回顯
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] });
刪除時間段映射
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操作的時候還需要考慮到阻止事件冒泡等情況),特別是頻繁更新時,并且具有完全自定義的繪制能力和更精確的觸摸事件處理。
到此這篇關于微信小程序日程預約的文章就介紹到這了,更多相關微信小程序日程預約內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript實現(xiàn)in-place思想的快速排序方法
這篇文章主要介紹了JavaScript實現(xiàn)in-place思想的快速排序方法的相關資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-08-08Javascript實現(xiàn)視頻輪播在pc端與移動端均可
用Javascript實現(xiàn)視頻輪播,畢竟是客戶的需求嗎?所以盡量實現(xiàn)下,下面有個實現(xiàn)視頻輪播的示例,pc端與移動端均可以實現(xiàn),感興趣的朋友可以了解下2013-09-09bootstrap下拉列表與輸入框組結合的樣式調(diào)整
輸入框組默認是div.input-group。接下來通過本文給大家介紹bootstrap下拉列表與輸入框組結合的樣式調(diào)整,感興趣的朋友一起看看吧2016-10-10詳解extract-text-webpack-plugin 的使用及安裝
這篇文章主要介紹了詳解extract-text-webpack-plugin 的使用及安裝,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06JavaScript實現(xiàn)簡單的四則運算計算器完整實例
這篇文章主要介紹了JavaScript實現(xiàn)簡單的四則運算計算器,結合完整實例形式分析了javascript基于表單相應實現(xiàn)加減乘除數(shù)學運算的操作技巧,需要的朋友可以參考下2017-04-04JavaScript函數(shù)節(jié)流和函數(shù)防抖之間的區(qū)別
本文主要介紹了JavaScript函數(shù)節(jié)流和函數(shù)防抖之間的區(qū)別。具有很好的參考價值,下面跟著小編一起來看下吧2017-02-02