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

需求分析
- 頂部七日選擇器
- 橫向顯示從當(dāng)前日期開始后的七天,并區(qū)分月-日
- 七天共計(jì)預(yù)約時(shí)間段綜合為3
- 中部canvas繪制區(qū)
- 左側(cè)時(shí)間刻度
- 右側(cè)繪制區(qū),總計(jì)24格,每大格為1h,一大格后期拆分四小格,為15min
- 右側(cè)繪制區(qū)功能
- 激活:單擊
- 長按:拖動(dòng)激活區(qū)域移動(dòng)選區(qū),存在激活區(qū)域之間的互斥
- 拉伸:雙擊后改變預(yù)約起止時(shí)間
- 底部數(shù)據(jù)回顯區(qū)
- 顯示預(yù)約時(shí)間段
- 支持刪除
代碼實(shí)現(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進(jìn)行繪制。
初始化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添加點(diǎn)擊事件bindtouchstart="onCanvasClick"
獲取點(diǎn)擊坐標(biāo),并解析首次觸摸點(diǎn)的位置touch[0],clientX 和 clientY 是觸摸點(diǎn)在屏幕上的坐標(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;計(jì)算時(shí)間格
const hourIndex = Math.floor(y / this.data.hourHeight);
hourHeight: rect.height / 24,來自于initCanvas初始化時(shí),提前計(jì)算好的每個(gè)時(shí)間格的高度
獲取選中的時(shí)間段
const existingBlockIndex = this.data.selectedBlocks.findIndex(block =>
hourIndex >= block.startHour && hourIndex < block.endHour
);使用 findIndex 查找點(diǎn)擊位置是否在已選時(shí)間段內(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í)需要考慮兩個(gè)數(shù)組:當(dāng)前日期選中時(shí)間段selectedBlocks,七日內(nèi)選中時(shí)間段總數(shù)allSelectedBlocks
新增時(shí)間段邏輯
else {
// 檢查限制
if (this.data.allSelectedBlocks.length >= 3) {
wx.showToast({
title: '最多只能選擇3個(gè)時(shí)間段',
icon: 'none'
});
return;
}
// 添加新時(shí)間段
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]
});
}先檢查是否達(dá)到最大選擇限制,創(chuàng)建新的時(shí)間段對(duì)象
date: 當(dāng)前選中的日期 startHour: 開始小時(shí) endHour: 結(jié)束小時(shí) startTime: 格式化后的開始時(shí)間 endTime: 格式化后的結(jié)束時(shí)間
2. 時(shí)間塊拉伸邏輯
檢測(cè)拉伸手柄
為了避免和后期的長按拖動(dòng)邏輯的沖突,在選中時(shí)間塊上額外添加上下手柄以作區(qū)分:
checkResizeHandle(x, y) {
const handleSize = 16; // 手柄的點(diǎn)擊范圍大小
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;
// 檢查是否點(diǎn)擊到上方手柄
if (y >= startY - handleSize && y <= startY + handleSize) {
return { blockIndex: i, isStart: true, position: startY };
}
// 檢查是否點(diǎn)擊到下方手柄
if (y >= endY - handleSize && y <= endY + handleSize) {
return { blockIndex: i, isStart: false, position: endY };
}
}
return null;
}處理拖拽拉伸邏輯
在判斷確定點(diǎn)擊到拉伸手柄的情況下,處理邏輯
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:開始拖動(dòng)的位置 initialY:手柄的初始位置 isResizingStart:是否在調(diào)整開始時(shí)間
處理拖動(dòng)過程
需要根據(jù)拖動(dòng)的距離來計(jì)算新的時(shí)間,將拖動(dòng)的距離轉(zhuǎn)換成時(shí)間的變化。簡單來說,假設(shè)一小時(shí)占60px的高度,那么15min=15px,如果用戶往下拖動(dòng)30px,換算成時(shí)間就是30min。
// 計(jì)算拖動(dòng)了多遠(yuǎn) const deltaY = currentY - startY; // 比如拖動(dòng)了30像素 // 計(jì)算15分鐘對(duì)應(yīng)的高度 const quarterHeight = hourHeight / 4; // 假設(shè)hourHeight是60,那么這里是15 // 計(jì)算移動(dòng)了多少個(gè)15分鐘 const quarterMoved = Math.floor(Math.abs(deltaY) / quarterHeight) * (deltaY > 0 ? 1 : -1); // 計(jì)算新的時(shí)間 const newTime = originalTime + (quarterMoved * 0.25); // 0.25代表15分鐘
更新時(shí)間顯示
計(jì)算出新的時(shí)間后,需要在確保有效范圍內(nèi)的同時(shí),對(duì)齊15min的刻度并轉(zhuǎn)化顯示格式
// 確保時(shí)間合理,比如不能小于0點(diǎn),不能超過24點(diǎn)
if (newTime >= 0 && newTime <= 24) {
// 對(duì)齊到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)松手時(shí),清楚拖動(dòng)狀態(tài),將標(biāo)識(shí)符置false
this.setData({
isResizing: false, // 結(jié)束拖動(dòng)狀態(tài)
resizingBlockIndex: null, // 清除正在拖動(dòng)的時(shí)間塊
startY: 0 // 重置起始位置
});
3. 時(shí)間塊拖動(dòng)邏輯
長按時(shí)間塊
首先找到點(diǎn)擊的時(shí)間塊并存儲(chǔ)信息,在原視圖上”刪除“該時(shí)間塊,并標(biāo)記拖動(dòng)狀態(tài)
onCanvasLongPress(e) {
// 1. 先找到用戶點(diǎn)擊的是哪個(gè)時(shí)間塊
const hourIndex = Math.floor(y / this.data.hourHeight);
const pressedBlockIndex = this.data.selectedBlocks.findIndex(block =>
hourIndex >= block.startHour && hourIndex < block.endHour
);
// 2. 如果真的點(diǎn)到了時(shí)間塊
if (pressedBlockIndex !== -1) {
// 3. 保存這個(gè)時(shí)間塊的信息,因?yàn)榇龝?huì)要用
const pressedBlock = {...this.data.selectedBlocks[pressedBlockIndex]};
// 4. 從原來的位置刪除這個(gè)時(shí)間塊
const newBlocks = [...this.data.selectedBlocks];
newBlocks.splice(pressedBlockIndex, 1);
// 5. 設(shè)置拖動(dòng)狀態(tài)
this.setData({
isDragging: true, // 標(biāo)記正在拖動(dòng)
dragBlock: pressedBlock, // 保存被拖動(dòng)的時(shí)間塊
dragStartY: y, // 記錄開始拖動(dòng)的位置
selectedBlocks: newBlocks, // 更新剩下的時(shí)間塊
dragBlockDuration: pressedBlock.endHour - pressedBlock.startHour // 記錄時(shí)間塊長度
});
}
}時(shí)間塊投影
為了區(qū)分正常激活時(shí)間塊,將長按的以投影虛化方式顯示,提示拖動(dòng)結(jié)束的位置。
首先計(jì)算觸摸移動(dòng)的距離,并根據(jù)上文,推測(cè)相應(yīng)時(shí)間變化。在合理的范圍內(nèi),檢測(cè)是否和其他時(shí)間塊互斥,最終更新時(shí)間塊的顯示。
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 // 是否顯示沖突警告
});
}
}互斥檢測(cè)
排除掉當(dāng)前拖動(dòng)時(shí)間塊,檢測(cè)與其余是否重疊。
具體來說,假設(shè)當(dāng)前時(shí)間塊9:00-10:00,新位置9:30-10:30,這種情況 startHour(9:30) < block.endHour(10:00) , endHour(10:30) > block.startHour(9:00)所以檢測(cè)為重疊
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)
當(dāng)位置不互斥,區(qū)域有效的情況下,放置新的時(shí)間塊,并添加到列表中,最后清理所有拖動(dòng)相關(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: '該時(shí)間段已被占用',
icon: 'none'
});
}
this.setData({
isDragging: false,
dragBlock: null,
dragStartY: 0,
dragCurrentY: 0,
dragShadowHour: null,
dragBlockDuration: null,
dragShadowWarning: false
});
}
}三、底部數(shù)據(jù)回顯
就是基本的數(shù)據(jù)更新回顯,setData
新增時(shí)間段回顯
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]
});刪除時(shí)間段映射
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控制時(shí)間塊的操作,canvas的渲染性能更好,交互也也更加靈活(dom操作的時(shí)候還需要考慮到阻止事件冒泡等情況),特別是頻繁更新時(shí),并且具有完全自定義的繪制能力和更精確的觸摸事件處理。
到此這篇關(guān)于微信小程序日程預(yù)約的文章就介紹到這了,更多相關(guān)微信小程序日程預(yù)約內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS 實(shí)現(xiàn)banner圖片輪播效果(鼠標(biāo)事件)
js實(shí)現(xiàn)banner圖片輪播效果,通過鼠標(biāo)點(diǎn)擊左右可切換圖片,具體實(shí)現(xiàn)代碼大家參考下本文2017-08-08
weui中的picker使用js進(jìn)行動(dòng)態(tài)綁定數(shù)據(jù)問題
這篇文章主要介紹了weui中的picker使用js進(jìn)行動(dòng)態(tài)綁定數(shù)據(jù)問題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11
javascript回車完美實(shí)現(xiàn)tab切換功能
這篇文章主要介紹了javascript通過回車實(shí)現(xiàn)tab切換功能,需要的朋友可以參考下2014-03-03
JavaScript自定義localStorage監(jiān)聽事件的解決方法
在項(xiàng)目開發(fā)過程中,發(fā)現(xiàn)有很多時(shí)候進(jìn)行l(wèi)ocalStorage.setItem()操作設(shè)置本地存儲(chǔ)后,頁面必須刷新才能夠獲取到存儲(chǔ)數(shù)據(jù),為了解決這個(gè)問題,就必須要用到自定義localStorage監(jiān)聽事件了,所以本文給大家介紹了自定義localStorage監(jiān)聽事件,需要的朋友可以參考下2024-10-10
Typescript學(xué)習(xí)之接口interface舉例詳解
TypeScript是JavaScript的一個(gè)超集,主要提供了類型系統(tǒng)和對(duì)ES6+的支持,TypeScript的核心原則之一是對(duì)值所具有的結(jié)構(gòu)進(jìn)行類型檢查,這篇文章主要給大家介紹了關(guān)于Typescript學(xué)習(xí)之接口interface的相關(guān)資料,需要的朋友可以參考下2024-03-03
BootStrap Validator使用注意事項(xiàng)(必看篇)
針對(duì)bootstrap2和bootstrap3有不同的版本,在使用bootstrap validator時(shí)需要了解其注意事項(xiàng),下面小編把我遇到的注意事項(xiàng)分享給大家,供大家參考2016-09-09
JS實(shí)現(xiàn)數(shù)組簡單去重及數(shù)組根據(jù)對(duì)象中的元素去重操作示例
這篇文章主要介紹了JS實(shí)現(xiàn)數(shù)組簡單去重及數(shù)組根據(jù)對(duì)象中的元素去重操作,涉及javascript數(shù)組元素的遍歷、判斷、追加等操作實(shí)現(xiàn)去重功能的相關(guān)技巧,需要的朋友可以參考下2018-01-01

