之前在外包做項目的時候,甲方提出一個想要合併單元格的需求,表格裏展示的內容是領導們的一週行程,因為不想出現重複內容的單元格,實際場景中領導可能連續幾天參加某個會議或者某個其他行程,本來 系統中對會議時間衝突是做了限制,也就是不能創建時間衝突的會議,那麼對重複行程的單元格直接進行合併是沒有問題的;但是後來又放開了限制、又允許存在會議時間衝突的情況了,因為實際中可能存在連續幾天的大會行程中,又安排了幾個小會,所以在後續的溝通中確定的方案是:單獨的連續行程進行合併,如果中間出現多個行程就不合並,如果單獨的長行程還沒結束,後面連續的排期還是合併。最終的效果參考下圖中的“會議111”。
根據表格的數據合併單元格,因為用的是ant design vue這個UI庫,所以我第一時間想的就是去翻文檔,查到的用法如下:
可是把這段代碼寫到項目裏並沒有生效,才發現最新已經是"ant-design-vue": "^4.2.6",而項目裏用的版本是"ant-design-vue": "^1.6.3",看懵了🤧🤧🤧,查了之後才發現這個版本的使用方法是這樣的:
於是我就按着這麼寫:
結果發現rowSpan的設置不管用,在網上搜索了一番,又自己試了幾次,發現加上style的設置才實現了合併單元格。
很煩接手這種項目,總是用一套模板開發新項目,永遠不更新三方庫,大量公司的“降本增效”以後這種情況會越來越多吧,反正當下能用就行,以後維護不了了再去考慮更新三方庫不知道會爆出什麼問題呢😅
具體的單元格是否合併就是按照業務邏輯來判斷了。在這個項目裏,每日行程的原始數據結構類似如下,就是把每個領導本週內的行程給查詢出來。
{
staff1: [
{
event: '會議111',
startTime: '2025-01-01 9:00',
endTime: '2025-01-04 18: 00'
},
{
event: '這是一個測試會議22',
startTime: '2025-01-02 13:00',
endTime: '2025-01-02 16: 00'
},
{
event: '這是一個測試會議33',
startTime: '2025-01-05 09:00',
endTime: '2025-01-05 17: 00'
}
],
staff2: [
{
event: '會議q',
startTime: '2025-01-01 9:00',
endTime: '2025-01-01 18: 00'
},
{
event: '這是一個測試會議ww',
startTime: '2025-01-02 13:00',
endTime: '2025-01-07 16: 00'
},
],
staff3: [
{
event: '待辦事項x',
startTime: '2025-01-01 9:00',
endTime: '2025-01-01 18: 00'
},
{
event: '這是一個待辦事項ww',
startTime: '2025-01-05 13:00',
endTime: '2025-01-07 16: 00'
},
]
}
後端會做簡單的處理,把日程按單日分組,返回給前端的數據結構類似如下(項目裏原本是week0~week6,本文簡單演示就直接使用日期了):
{
staff1: {
'2025-01-01': [
{
event: '會議111',
startTime: '2025-01-01 9:00',
endTime: '2025-01-04 18: 00'
},
],
'2025-01-02': [
{
event: '會議111',
startTime: '2025-01-01 9:00',
endTime: '2025-01-04 18: 00'
},
{
event: '這是一個測試會議22',
startTime: '2025-01-02 13:00',
endTime: '2025-01-02 16: 00'
},
],
'2025-01-03': [
{
event: '會議111',
startTime: '2025-01-01 9:00',
endTime: '2025-01-04 18: 00'
},
],
'2025-01-04': [
{
event: '會議111',
startTime: '2025-01-01 9:00',
endTime: '2025-01-04 18: 00'
},
],
'2025-01-05': [
{
event: '這是一個測試會議33',
startTime: '2025-01-05 09:00',
endTime: '2025-01-05 17: 00'
}
],
'2025-01-06': [],
'2025-01-07': [],
},
staff2: {
'2025-01-01': [
{
event: '會議q',
startTime: '2025-01-01 9:00',
endTime: '2025-01-01 18: 00'
},
],
'2025-01-02': [
{
event: '這是一個測試會議ww',
startTime: '2025-01-02 13:00',
endTime: '2025-01-07 16: 00'
},
],
'2025-01-03': [
{
event: '這是一個測試會議ww',
startTime: '2025-01-02 13:00',
endTime: '2025-01-07 16: 00'
},
],
'2025-01-04': [
{
event: '這是一個測試會議ww',
startTime: '2025-01-02 13:00',
endTime: '2025-01-07 16: 00'
},
],
'2025-01-05': [
{
event: '這是一個測試會議ww',
startTime: '2025-01-02 13:00',
endTime: '2025-01-07 16: 00'
},
],
'2025-01-06': [
{
event: '這是一個測試會議ww',
startTime: '2025-01-02 13:00',
endTime: '2025-01-07 16: 00'
},
],
'2025-01-07': [
{
event: '這是一個測試會議ww',
startTime: '2025-01-02 13:00',
endTime: '2025-01-07 16: 00'
},
],
},
staff3: {
'2025-01-01': [
{
event: '待辦事項x',
startTime: '2025-01-01 9:00',
endTime: '2025-01-01 18: 00'
},
],
'2025-01-02': [],
'2025-01-03': [],
'2025-01-04': [],
'2025-01-05': [
{
event: '這是一個待辦事項ww',
startTime: '2025-01-05 13:00',
endTime: '2025-01-07 16: 00'
},
],
'2025-01-06': [
{
event: '這是一個待辦事項ww',
startTime: '2025-01-05 13:00',
endTime: '2025-01-07 16: 00'
},
],
'2025-01-07': [
{
event: '這是一個待辦事項ww',
startTime: '2025-01-05 13:00',
endTime: '2025-01-07 16: 00'
},
],
},
}
前端就在以上的結構基礎上進行遍歷處理。
第一步準備工作,先簡單判斷當前處理的行程是否在一天內結束,並且判斷是否跨時段(上下午),把這個兩個判斷結果存儲起來用於後續操作。
const inOneDay =
moment(schedule.endTime).format('YYYY-MM-DD') ===
moment(schedule.startTime).format('YYYY-MM-DD') // 是否在一天內完成(開始日期和結束日期一致)
let inOneRange = false // 是否在同個時段(上下午),判斷一天內的日程是否跨時段
if (inOneDay) {
const startMorning = moment(schedule.startTime).isSameOrBefore(
weekData[weekIndex].dateStr + ' ' + MORNING_END
)
const endAfternoon = moment(schedule.endTime).isSameOrAfter(
weekData[weekIndex].dateStr + ' ' + AFTERNOON_START
)
if ((startMorning && !endAfternoon) || (!startMorning && endAfternoon)) inOneRange = true
}
第二步就在第一步的基礎上先做第一輪簡單的篩選,如果滿足以下條件之一,則當前處理的行程不用跨行處理。
- 當前行程所在時段存在多個行程
- 當前行程本身不跨時段
- 當前行程跨上下午時段,當前處理的是下午,但是上午存在多個行程
if (
weekData[weekIndex][account].length > 1 || // 當前員工單個時段有多個行程
(inOneDay && inOneRange) || // 某行程不跨時段
(inOneDay &&
!inOneRange &&
weekIndex % 2 === 1 &&
weekData[weekIndex - 1][account].length > 1) // 當前行程跨上下午時段,當前處理的是下午,但是上午存在多個行程
) {
// 不做跨行處理
result.isCross = false
return result
}
第三步做第二輪篩選,首先做兩個判斷並保存判斷結果。
-
當前是否為跨行的開始行
// 判斷是否是跨行的開始(滿足條件之一): // 1. 行程的開始日期等於當前行的日期,行程的開始時間晚於等於當前行的startTime // 2. 行程的開始日期等於當前行的日期,行程的結束日期晚於當前行的日期 // 3. 行程的開始日期早於當前行的日期,且前一行的行程數量大於1 // 4. 當前行程在第一行 const isStart = (scheduleStartDate === weekData[weekIndex].dateStr && scheduleStartTime >= weekData[weekIndex].startTime) || (weekIndex % 2 === 1 && weekData[weekIndex - 1][account].length > 1 && scheduleStartDate === weekData[weekIndex].dateStr && scheduleEndDate >= weekData[weekIndex].dateStr) || (scheduleStartDate < weekData[weekIndex].dateStr && weekIndex > 0 && weekData[weekIndex - 1][account].length > 1) || weekIndex === 0 -
當前是否為跨行的結束行
// 判斷是否是跨行的結束(滿足條件之一): // 1. 當前行程在最後一行 // 2. 行程的結束日期等於當前行的日期,行程的結束時間晚於當前行的startTime且早於等於當前行的endTime // 3. 下一行的日程數量大於1 const isEnd = weekIndex === 13 || (scheduleEndDate === weekData[weekIndex].dateStr && scheduleEndTime >= weekData[weekIndex].startTime && scheduleEndTime <= weekData[weekIndex].endTime) || weekData[weekIndex + 1][account].length > 1
如果兩個判斷結果都為true,則説明既是開始行,同是又是結束行,那就不用做跨行處理。
最後篩出來的就是要跨行的單元格了,就要計算跨的行數了,也就是起始行的rowSpan值,非起始行的rowSpan就是0了。
起始行的rowSpan就是計算具體這個行程在表格裏跨的行數。
首先計算單個行程自身原本跨了幾個時段。
const diffScheduleEnd = moment(scheduleEndDate).diff(
moment(weekData[weekIndex].dateStr),
'days'
) // 與行程結束日期的天數差值
const diffWeekEnd = moment(weekData[13].dateStr).diff(
moment(weekData[weekIndex].dateStr),
'days'
) // 與周最後一天的天數差值
const dayOff = Math.min(diffScheduleEnd, diffWeekEnd) // 跨的天數
const timeOff = scheduleEndTime <= MORNING_END ? 1 : 2 // 跨的時段
let offRows = 0
// 行位移 = (天數-1)*2 + 跨的時段
if (dayOff > 0) offRows = (dayOff - 1) * 2 + timeOff
// 如果當前行程是上午開始的,再加一個行跨
if (weekIndex % 2 === 0) offRows++
再向後遍歷碰到存在多個行程的單元格就表示跨行結束,得到了rowSpan的值。
const len = weekIndex + 1 + offRows
let rowSpan = 1
for (let i = weekIndex + 1; i < len; i++) {
if (weekData[i][account].length > 1) {
break
} else {
rowSpan++
}
}
最後我們就可以得到合併的單元格。