|
|
|
|
@ -4,7 +4,7 @@
|
|
|
|
|
<view class="nav-btn" @tap="prevMonth">‹</view>
|
|
|
|
|
<text class="month-text">{{ displayYear }}年{{ displayMonthText }}月</text>
|
|
|
|
|
<view class="nav-btn" @tap="nextMonth">›</view>
|
|
|
|
|
<text class="back-today" @tap="backToday">今天</text>
|
|
|
|
|
<!-- <text class="back-today" @tap="backToday">今天</text> -->
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<view class="weekdays">
|
|
|
|
|
@ -16,8 +16,8 @@
|
|
|
|
|
<view class="row" v-for="(row, rIdx) in weeks" :key="rIdx">
|
|
|
|
|
<view class="cell" v-for="cell in row" :key="cell.fullDate" :style="'height:'+cellHeight+'rpx'" @tap="onDayClick(cell.fullDate)">
|
|
|
|
|
<text class="date-num" :class="{ dim: !cell.inMonth }">{{ cell.date }}</text>
|
|
|
|
|
<view class="cell-events">
|
|
|
|
|
<view v-for="ev in eventsForDate(cell.fullDate)" :key="ev.id" class="event-chip" :class="'event-type-' + (ev.type || 'default')" :style="ev && ev.color ? ('background:'+ev.color+';') : ''" @tap="onEventClick(ev)">
|
|
|
|
|
<view class="cell-events" :style="'padding-top:'+getCellPadding(cell.fullDate)+'rpx'">
|
|
|
|
|
<view v-for="ev in eventsForDate(cell.fullDate)" :key="ev.id" class="event-chip" :class="['event-type-' + (ev.type || 'default'), { 'single-line-open': isSingleEvent(cell.fullDate), 'conflict': hasSpanConflict(cell.fullDate) }]" :style="'background:' + ((ev && ev.color) ? ev.color : '#ddba99') + ';'+ (hasSpanConflict(cell.fullDate) ? 'margin-top:6rpx;' : '')" @tap="onEventClick(ev)">
|
|
|
|
|
{{ formatTitle(ev.title) }}
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
@ -26,7 +26,7 @@
|
|
|
|
|
|
|
|
|
|
<!-- 跨天覆盖层 -->
|
|
|
|
|
<view class="overlay">
|
|
|
|
|
<view v-for="(seg, si) in continuousSegments" :key="si" class="continuous-bar" :style="'left:'+seg._style.left+';width:'+seg._style.width+';top:'+seg._style.top+';height:'+seg._style.height+';line-height:'+seg._style.height+';'+(seg.color?('background:'+seg.color+';'):'')" :class="'event-type-' + (seg.type || 'default')" @tap="onSegmentClick(seg)">
|
|
|
|
|
<view v-for="(seg, si) in continuousSegments" :key="si" class="continuous-bar" :style="'left:'+seg._style.left+';width:'+seg._style.width+';top:'+seg._style.top+';height:'+seg._style.height+';'+(seg._isOnlyOne ? '' : ('line-height:'+seg._style.height+';'))+(seg.color?('background:'+seg.color+';'):'background:#ddba99;')" :class="['event-type-' + (seg.type || 'default'), { 'nobreak': seg._isOnlyOne } ]" @tap="onSegmentClick(seg)">
|
|
|
|
|
{{ formatTitle(seg.title) }}
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
@ -43,6 +43,7 @@ export default {
|
|
|
|
|
type: String,
|
|
|
|
|
required: true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
events: { // [{ id, title, type, start_time, end_time }]
|
|
|
|
|
type: Array,
|
|
|
|
|
default: () => []
|
|
|
|
|
@ -137,7 +138,7 @@ export default {
|
|
|
|
|
return ['日','一','二','三','四','五','六']
|
|
|
|
|
},
|
|
|
|
|
weeks() {
|
|
|
|
|
// 6x7 网格
|
|
|
|
|
// 动态计算需要的行数
|
|
|
|
|
const firstDay = new Date(this.baseDate)
|
|
|
|
|
if (!(firstDay instanceof Date) || isNaN(firstDay.getTime())) return []
|
|
|
|
|
const startDow = firstDay.getDay()
|
|
|
|
|
@ -145,8 +146,16 @@ export default {
|
|
|
|
|
const gridStart = new Date(firstDay)
|
|
|
|
|
gridStart.setDate(1 - offset)
|
|
|
|
|
|
|
|
|
|
// 计算当月最后一天
|
|
|
|
|
const lastDay = new Date(this.baseDate.getFullYear(), this.baseDate.getMonth() + 1, 0)
|
|
|
|
|
const lastDayOfMonth = lastDay.getDate()
|
|
|
|
|
|
|
|
|
|
// 计算需要的行数
|
|
|
|
|
const totalDays = offset + lastDayOfMonth
|
|
|
|
|
const neededWeeks = Math.ceil(totalDays / 7)
|
|
|
|
|
|
|
|
|
|
const weeks = []
|
|
|
|
|
for (let w = 0; w < 6; w += 1) {
|
|
|
|
|
for (let w = 0; w < neededWeeks; w += 1) {
|
|
|
|
|
const row = []
|
|
|
|
|
for (let d = 0; d < 7; d += 1) {
|
|
|
|
|
const cur = new Date(gridStart)
|
|
|
|
|
@ -164,8 +173,9 @@ export default {
|
|
|
|
|
return weeks
|
|
|
|
|
},
|
|
|
|
|
gridHeightPx() {
|
|
|
|
|
// 返回 rpx 数值
|
|
|
|
|
return this.headerHeight + this.weekHeaderHeight + this.cellHeight * 6
|
|
|
|
|
// 返回 rpx 数值,根据实际行数计算
|
|
|
|
|
const actualWeeks = this.weeks.length
|
|
|
|
|
return this.headerHeight + this.weekHeaderHeight + this.cellHeight * actualWeeks
|
|
|
|
|
},
|
|
|
|
|
continuousSegments() {
|
|
|
|
|
// 拆分跨天事件为每周分段
|
|
|
|
|
@ -226,8 +236,23 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 统计跨天分段对每日的覆盖数量
|
|
|
|
|
const segCountByDate = {}
|
|
|
|
|
const addDateKey = (ms) => {
|
|
|
|
|
const d = new Date(ms)
|
|
|
|
|
d.setHours(0,0,0,0)
|
|
|
|
|
const key = `${d.getFullYear()}-${this.pad2(d.getMonth()+1)}-${this.pad2(d.getDate())}`
|
|
|
|
|
segCountByDate[key] = (segCountByDate[key] || 0) + 1
|
|
|
|
|
}
|
|
|
|
|
segs.forEach(seg => {
|
|
|
|
|
for (let t = seg.segStartMs; t <= seg.segEndMs; t += 86400000) {
|
|
|
|
|
addDateKey(t)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为每周的分段分配 lane,避免重叠
|
|
|
|
|
const byWeek = {}
|
|
|
|
|
const laneCountByDate = {}
|
|
|
|
|
segs.forEach(s => {
|
|
|
|
|
const key = s.weekStartISO
|
|
|
|
|
if (!byWeek[key]) byWeek[key] = []
|
|
|
|
|
@ -251,6 +276,12 @@ export default {
|
|
|
|
|
seg.laneIndex = laneEnd.length
|
|
|
|
|
laneEnd.push(seg.endCol)
|
|
|
|
|
}
|
|
|
|
|
// 记录该分段覆盖到的每日所需预留层数
|
|
|
|
|
for (let t = seg.segStartMs; t <= seg.segEndMs; t += 86400000) {
|
|
|
|
|
const d = new Date(t)
|
|
|
|
|
const key = `${d.getFullYear()}-${this.pad2(d.getMonth()+1)}-${this.pad2(d.getDate())}`
|
|
|
|
|
laneCountByDate[key] = Math.max(laneCountByDate[key] || 0, seg.laneIndex + 1)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@ -289,20 +320,58 @@ export default {
|
|
|
|
|
const widthPct = seg.spanCols * cellWidthPct
|
|
|
|
|
const vOffset = (seg.laneIndex || 0) * (this.barHeight + this.barSpacing)
|
|
|
|
|
const innerTopPadding = 8 // 额外内边距,避免贴近格子顶部
|
|
|
|
|
const topRpx = (row * this.cellHeight) + this.dateNumberHeight + innerTopPadding + vOffset - 10
|
|
|
|
|
const topRpx = (row * this.cellHeight) + this.dateNumberHeight + innerTopPadding + vOffset - 3
|
|
|
|
|
|
|
|
|
|
// 确保事件不会超出日历边界
|
|
|
|
|
const maxLeftPct = 100 - widthPct
|
|
|
|
|
const finalLeftPct = Math.min(leftPct, maxLeftPct)
|
|
|
|
|
|
|
|
|
|
seg._style = {
|
|
|
|
|
left: leftPct + '%',
|
|
|
|
|
left: finalLeftPct + '%',
|
|
|
|
|
width: widthPct + '%',
|
|
|
|
|
top: topRpx + 'rpx',
|
|
|
|
|
height: heightRpx + 'rpx'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 判断该跨天分段在其覆盖的日期是否独占(无单天事件、无其它跨天分段)
|
|
|
|
|
let onlyOne = true
|
|
|
|
|
for (let t = seg.segStartMs; t <= seg.segEndMs; t += 86400000) {
|
|
|
|
|
const d = new Date(t)
|
|
|
|
|
const key = `${d.getFullYear()}-${this.pad2(d.getMonth()+1)}-${this.pad2(d.getDate())}`
|
|
|
|
|
const segCnt = segCountByDate[key] || 0
|
|
|
|
|
const singleCnt = (this.eventsForDate(key) || []).length // 单天事件数量
|
|
|
|
|
if (!(segCnt === 1 && singleCnt === 0)) { onlyOne = false; break }
|
|
|
|
|
}
|
|
|
|
|
seg._isOnlyOne = onlyOne
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 暴露给实例:为单日事件预留空间 & 判定与跨天冲突
|
|
|
|
|
this.laneCountByDate = laneCountByDate
|
|
|
|
|
this.segCountByDate = segCountByDate
|
|
|
|
|
return segs
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
getCellPadding(fullDate) {
|
|
|
|
|
// 根据覆盖该日期的跨天事件层数,为单日事件预留顶部空间,避免与跨天条重叠
|
|
|
|
|
const lanes = this.laneCountByDate && this.laneCountByDate[fullDate] ? this.laneCountByDate[fullDate] : 0
|
|
|
|
|
if (!lanes) return 0
|
|
|
|
|
return lanes * (this.barHeight + this.barSpacing)
|
|
|
|
|
},
|
|
|
|
|
isSingleEvent(fullDate) {
|
|
|
|
|
try {
|
|
|
|
|
const list = this.eventsForDate(fullDate) || []
|
|
|
|
|
return list.length === 1
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
hasSpanConflict(fullDate) {
|
|
|
|
|
// 有跨天覆盖且该日也有单日事件
|
|
|
|
|
const hasMulti = !!(this.segCountByDate && this.segCountByDate[fullDate] > 0)
|
|
|
|
|
const hasSingle = (this.eventsForDate(fullDate) || []).length > 0
|
|
|
|
|
return hasMulti && hasSingle
|
|
|
|
|
},
|
|
|
|
|
onEventClick(ev) {
|
|
|
|
|
this.$emit('eventClick', ev)
|
|
|
|
|
},
|
|
|
|
|
@ -413,30 +482,33 @@ export default {
|
|
|
|
|
<style scoped>
|
|
|
|
|
.calendar-grid {
|
|
|
|
|
background: #fff;
|
|
|
|
|
// border-radius: 18rpx;
|
|
|
|
|
/* border-radius: 18rpx; */
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
|
|
|
border-radius: 0 0 20rpx 20rpx;
|
|
|
|
|
/* margin: 20rpx;
|
|
|
|
|
padding: 10rpx 0 20rpx 0; */
|
|
|
|
|
}
|
|
|
|
|
.calendar-header {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
height: 46px;
|
|
|
|
|
border-bottom: 1px solid #ededed;
|
|
|
|
|
background:#eaf3fb;
|
|
|
|
|
color:#0d0398;
|
|
|
|
|
font-size:34rpx;
|
|
|
|
|
}
|
|
|
|
|
.nav-btn {
|
|
|
|
|
width: 40px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 50rpx;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
.month-text {
|
|
|
|
|
width: 140px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
color: #333;
|
|
|
|
|
font-size: 34rpx;
|
|
|
|
|
color:#0d0398;
|
|
|
|
|
}
|
|
|
|
|
.back-today {
|
|
|
|
|
position: absolute;
|
|
|
|
|
@ -452,15 +524,21 @@ export default {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
color: #0d0398;
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
}
|
|
|
|
|
.weekday {
|
|
|
|
|
width: 14.2857%;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
line-height: 40px;
|
|
|
|
|
color: #0d0398;
|
|
|
|
|
border-right: 1px solid #f2eae2;
|
|
|
|
|
}
|
|
|
|
|
.weekday:last-child {
|
|
|
|
|
border-right:none
|
|
|
|
|
}
|
|
|
|
|
.grid {
|
|
|
|
|
position: relative;
|
|
|
|
|
@ -472,46 +550,76 @@ export default {
|
|
|
|
|
.cell {
|
|
|
|
|
width: 14.2857%;
|
|
|
|
|
height: 120rpx;
|
|
|
|
|
border-bottom: 1px solid #f5f5f5;
|
|
|
|
|
border-top: 1px solid #f5f5f5;
|
|
|
|
|
border-right: 1px solid #f5f5f5;
|
|
|
|
|
border-top: 1px solid #f2eae2;
|
|
|
|
|
border-right: 1px solid #f2eae2;
|
|
|
|
|
position: relative;
|
|
|
|
|
padding: 2px 2px 2px 2px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
}
|
|
|
|
|
.row .cell:first-child {
|
|
|
|
|
border-left: 1px solid #f5f5f5;
|
|
|
|
|
.row .cell:last-child {
|
|
|
|
|
border-right:none
|
|
|
|
|
}
|
|
|
|
|
.date-num {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #333;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 3;
|
|
|
|
|
text-align: center;
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
.date-num.dim {
|
|
|
|
|
color: #bfbfbf;
|
|
|
|
|
}
|
|
|
|
|
.cell-events {
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 3;
|
|
|
|
|
z-index: 2; /* 让跨天条位于其上方 */
|
|
|
|
|
width: 100%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.event-chip {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
line-height: 14px;
|
|
|
|
|
padding: 1px 3px;
|
|
|
|
|
padding: 2rpx 10rpx;
|
|
|
|
|
margin: 1px 0;
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
.event-chip.event-type-1 { background: #67C23A; }
|
|
|
|
|
.event-chip.event-type-2 { background: #409EFF; }
|
|
|
|
|
.event-chip.event-type-3 { background: #E6A23C; }
|
|
|
|
|
.event-chip.event-type-4 { background: #F56C6C; }
|
|
|
|
|
.event-chip.event-type-5 { background: #909399; }
|
|
|
|
|
.event-chip.event-type-default { background: #409EFF; }
|
|
|
|
|
.event-chip.event-type-default { background: #ddba99; }
|
|
|
|
|
/* 当天只有一条单天事件时,允许换行不省略 */
|
|
|
|
|
.event-chip.single-line-open {
|
|
|
|
|
white-space: normal;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 3; /* 最多三行 */
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
/* 与跨天冲突时,单日事件强制两行省略并稍作下移 */
|
|
|
|
|
.event-chip.conflict {
|
|
|
|
|
white-space: normal !important;
|
|
|
|
|
display: -webkit-box !important;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
-webkit-line-clamp: 2 !important;
|
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
text-overflow: ellipsis !important;
|
|
|
|
|
margin-top: 6rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overlay {
|
|
|
|
|
position: absolute;
|
|
|
|
|
@ -520,7 +628,7 @@ export default {
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
z-index: 4; /* 置于单日事件之上,避免被遮挡 */
|
|
|
|
|
}
|
|
|
|
|
.continuous-bar {
|
|
|
|
|
position: absolute;
|
|
|
|
|
@ -529,20 +637,35 @@ export default {
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
padding: 0rpx 10rpx;
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
|
|
|
background: #409EFF;
|
|
|
|
|
background: #ddba99;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
.continuous-bar.event-type-1 { background: linear-gradient(90deg, #67C23A 0%, #5CB85C 100%); }
|
|
|
|
|
.continuous-bar.event-type-2 { background: linear-gradient(90deg, #409EFF 0%, #337ecc 100%); }
|
|
|
|
|
.continuous-bar.event-type-3 { background: linear-gradient(90deg, #E6A23C 0%, #D4952B 100%); }
|
|
|
|
|
.continuous-bar.event-type-4 { background: linear-gradient(90deg, #F56C6C 0%, #E85555 100%); }
|
|
|
|
|
.continuous-bar.event-type-5 { background: linear-gradient(90deg, #909399 0%, #73767A 100%); }
|
|
|
|
|
.continuous-bar.event-type-default { background: linear-gradient(90deg, #409EFF 0%, #337ecc 100%); }
|
|
|
|
|
.continuous-bar.event-type-default { background: #ddba99; }
|
|
|
|
|
/* 跨天事件在整段期间独占时不省略,允许换行 */
|
|
|
|
|
.continuous-bar.nobreak {
|
|
|
|
|
white-space: normal;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
height: auto !important; /* 允许多行内容自适应高度 */
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
padding-top: 6rpx;
|
|
|
|
|
padding-bottom: 6rpx;
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 3; /* 最多三行 */
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|