微信小程序日程預(yù)約功能實現(xiàn)
涉及儀器的預(yù)約使用,仿照小米日歷日程預(yù)約開發(fā)開發(fā)對應(yīng)頁。
效果展示

需求分析
- 頂部七日選擇器
- 橫向顯示從當(dāng)前日期開始后的七天,并區(qū)分月-日
- 七天共計預(yù)約時間段綜合為3
- 中部canvas繪制區(qū)
- 左側(cè)時間刻度
- 右側(cè)繪制區(qū),總計24格,每大格為1h,一大格后期拆分四小格,為15min
- 右側(cè)繪制區(qū)功能
- 激活:單擊
- 長按:拖動激活區(qū)域移動選區(qū),存在激活區(qū)域之間的互斥
- 拉伸:雙擊后改變預(yù)約起止時間
- 底部數(shù)據(jù)回顯區(qū)
- 顯示預(yù)約時間段
- 支持刪除
代碼實現(xiàn)
一、構(gòu)建基礎(chǔ)頁面結(jié)構(gòu)

1. 頂部日期選擇器
獲取當(dāng)前日期,即六天后的所有日期,并解析出具體月-日,存入數(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,只作為標(biāo)志數(shù)據(jù);【主體】右側(cè)24格,通過canvas進行繪制。
初始化canvas,獲取寬高,并通過ctx.scale(dpr,dpr)縮放canvas適應(yīng)設(shè)備像素比;
繪制網(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功能細(xì)分
1. 激活狀態(tài)的判斷
首先給canvas添加點擊事件bindtouchstart="onCanvasClick"
獲取點擊坐標(biāo),并解析首次觸摸點的位置touch[0],clientX 和 clientY 是觸摸點在屏幕上的坐標(biāo)
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) {
// 從當(dāng)前日期的選中塊中移除
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ù)組:當(dāng)前日期選中時間段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: 當(dāng)前選中的日期 startHour: 開始小時 endHour: 結(jié)束小時 startTime: 格式化后的開始時間 endTime: 格式化后的結(jié)束時間
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:標(biāo)記正在拉伸 startY:開始拖動的位置 initialY:手柄的初始位置 isResizingStart:是否在調(diào)整開始時間
處理拖動過程
需要根據(jù)拖動的距離來計算新的時間,將拖動的距離轉(zhuǎn)換成時間的變化。簡單來說,假設(shè)一小時占60px的高度,那么15min=15px,如果用戶往下拖動30px,換算成時間就是30min。
// 計算拖動了多遠 const deltaY = currentY - startY; // 比如拖動了30像素 // 計算15分鐘對應(yīng)的高度 const quarterHeight = hourHeight / 4; // 假設(shè)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')}`;
}結(jié)束拉伸邏輯
當(dāng)松手時,清楚拖動狀態(tài),將標(biāo)識符置false
this.setData({
isResizing: false, // 結(jié)束拖動狀態(tài)
resizingBlockIndex: null, // 清除正在拖動的時間塊
startY: 0 // 重置起始位置
});
3. 時間塊拖動邏輯
長按時間塊
首先找到點擊的時間塊并存儲信息,在原視圖上”刪除“該時間塊,并標(biāo)記拖動狀態(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. 設(shè)置拖動狀態(tài)
this.setData({
isDragging: true, // 標(biāo)記正在拖動
dragBlock: pressedBlock, // 保存被拖動的時間塊
dragStartY: y, // 記錄開始拖動的位置
selectedBlocks: newBlocks, // 更新剩下的時間塊
dragBlockDuration: pressedBlock.endHour - pressedBlock.startHour // 記錄時間塊長度
});
}
}時間塊投影
為了區(qū)分正常激活時間塊,將長按的以投影虛化方式顯示,提示拖動結(jié)束的位置。
首先計算觸摸移動的距離,并根據(jù)上文,推測相應(yīng)時間變化。在合理的范圍內(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 // 是否顯示沖突警告
});
}
}互斥檢測
排除掉當(dāng)前拖動時間塊,檢測與其余是否重疊。
具體來說,假設(shè)當(dāng)前時間塊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);
});
}結(jié)束拖動
當(dāng)位置不互斥,區(qū)域有效的情況下,放置新的時間塊,并添加到列表中,最后清理所有拖動相關(guān)的狀態(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 });
}總結(jié)
相比于初版的div控制時間塊的操作,canvas的渲染性能更好,交互也也更加靈活(dom操作的時候還需要考慮到阻止事件冒泡等情況),特別是頻繁更新時,并且具有完全自定義的繪制能力和更精確的觸摸事件處理。
到此這篇關(guān)于微信小程序日程預(yù)約的文章就介紹到這了,更多相關(guān)微信小程序日程預(yù)約內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript實現(xiàn)in-place思想的快速排序方法
這篇文章主要介紹了JavaScript實現(xiàn)in-place思想的快速排序方法的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-08-08
Javascript實現(xiàn)視頻輪播在pc端與移動端均可
用Javascript實現(xiàn)視頻輪播,畢竟是客戶的需求嗎?所以盡量實現(xiàn)下,下面有個實現(xiàn)視頻輪播的示例,pc端與移動端均可以實現(xiàn),感興趣的朋友可以了解下2013-09-09
bootstrap下拉列表與輸入框組結(jié)合的樣式調(diào)整
輸入框組默認(rèn)是div.input-group。接下來通過本文給大家介紹bootstrap下拉列表與輸入框組結(jié)合的樣式調(diào)整,感興趣的朋友一起看看吧2016-10-10
詳解extract-text-webpack-plugin 的使用及安裝
這篇文章主要介紹了詳解extract-text-webpack-plugin 的使用及安裝,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
JavaScript實現(xiàn)簡單的四則運算計算器完整實例
這篇文章主要介紹了JavaScript實現(xiàn)簡單的四則運算計算器,結(jié)合完整實例形式分析了javascript基于表單相應(yīng)實現(xiàn)加減乘除數(shù)學(xué)運算的操作技巧,需要的朋友可以參考下2017-04-04
JavaScript函數(shù)節(jié)流和函數(shù)防抖之間的區(qū)別
本文主要介紹了JavaScript函數(shù)節(jié)流和函數(shù)防抖之間的區(qū)別。具有很好的參考價值,下面跟著小編一起來看下吧2017-02-02

