|
|
@@ -0,0 +1,793 @@
|
|
|
+<template>
|
|
|
+ <CalendarDateSelectHeader
|
|
|
+ v-if="showCalendarSelector"
|
|
|
+ v-model:currentYear="currentYear"
|
|
|
+ v-model:currentMonth="currentMonth"
|
|
|
+ ></CalendarDateSelectHeader>
|
|
|
+ <!-- 日历主体 -->
|
|
|
+ <!-- 日历网格(包含星期表头行+日期行) -->
|
|
|
+ <div class="cal_body" :style="{ gridTemplateRows: `50px repeat(${calendarRows}, 1fr)` }">
|
|
|
+ <!-- 星期表头行 -->
|
|
|
+ <div class="cal_header"
|
|
|
+ v-for="(d, idx) in weekdays" :key="d"
|
|
|
+ :class="{ is_weekend: idx === 0 || idx === 6 }">
|
|
|
+ {{ d }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 日期行(从索引 7 开始,前 7 个是表头) -->
|
|
|
+ <div class="cal_cell" v-for="(cell, idx) in calendarCells"
|
|
|
+ :key="'c' + idx" :class="{
|
|
|
+ is_other_month: !cell.inCurrentMonth,
|
|
|
+ is_today: cell.isToday,
|
|
|
+ is_selected: cell.isSelected,
|
|
|
+ is_weekend: cell.weekday === 0 || cell.weekday === 6,
|
|
|
+ is_last_row: cell.isLastRow}"
|
|
|
+ @click="handleCellClick(cell.date, cell.inCurrentMonth)">
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 日期信息 -->
|
|
|
+ <div class="cal_cell_top" v-if="cell.inCurrentMonth">
|
|
|
+ <span class="cal_cell_num">{{ cell.num < 10 ? '0' + cell.num : cell.num }}</span>
|
|
|
+ <span class="cal_cell_count" v-if="cell.events.length > 0">共{{ cell.events.length
|
|
|
+ }}项</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 日历事件 -->
|
|
|
+ <div class="cal_cell_events" v-if="cell.inCurrentMonth">
|
|
|
+ <div class="cal_event_item"
|
|
|
+ v-for="(ev, ei) in cell.events" :key="ev.id"
|
|
|
+ @click.stop="handleEventClick(cell.date, ev, ei)"
|
|
|
+ >
|
|
|
+ <div class="event_main" :class="{ is_selected: selectedEventId === ev.id }">
|
|
|
+ <span class="cal_event_dot"></span>
|
|
|
+ <span class="cal_event_text">{{ ev.title }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 日历事件弹窗 -->
|
|
|
+ <!-- 事件悬浮窗挂载到 cell 外,避免被 cal_cell_events 的 overflow 裁剪 -->
|
|
|
+ <div v-if="hoverState.dateKey === formatDate(cell.date) && hoverState.event"
|
|
|
+ class="event_popup"
|
|
|
+ :class="cell.weekday === 5 || cell.weekday === 6 ? 'popup_left' : 'popup_right'"
|
|
|
+ :style="{ top: `${10 + hoverState.eventIndex * 24}px` }">
|
|
|
+ <div class="event_popup_title">{{ hoverState.event.title }}</div>
|
|
|
+ <div class="event_popup_actions">
|
|
|
+ <span class="event_popup_date">{{ formatPopupDate(cell.date) }}</span>
|
|
|
+ <span class="event_actions_right" v-if="hasSchoolManager">
|
|
|
+ <span class="event_btn event_edit" @click.stop="editEvent(hoverState.event)">编辑</span>
|
|
|
+ <span class="event_btn event_delete"
|
|
|
+ @click.stop="deleteEvent(hoverState.event)">删除</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 创建事件 日期格子悬浮窗:展示当天所有日程列表 + 新增日程按钮 -->
|
|
|
+ <div v-if="cellHoverState.dateKey === formatDate(cell.date) && !hoverState.event"
|
|
|
+ class="cell_popup"
|
|
|
+ :class="[cell.weekday === 5 || cell.weekday === 6 ? 'popup_left' : 'popup_right', cell.isLastRow ? 'popup_top' : '']">
|
|
|
+ <div class="cell_popup_header">
|
|
|
+ <span class="cell_popup_date">{{ formatDate(cell.date) }}</span>
|
|
|
+ <span class="cell_popup_count">共{{ cell.events.length }}项</span>
|
|
|
+ </div>
|
|
|
+ <div class="cell_popup_events">
|
|
|
+ <div class="cell_popup_event" v-for="(ev, ei) in cell.events" :key="ev.id">
|
|
|
+ <span class="cell_popup_dot"></span>
|
|
|
+ <span class="cell_popup_text">{{ ev.title }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="cell.events.length === 0" class="cell_popup_empty">暂无日程安排</div>
|
|
|
+ </div>
|
|
|
+ <div class="cell_popup_add" v-if="hasSchoolManager" @click.stop="openAddSchedule(cell.date)">
|
|
|
+ <img :src="addIcon" alt="" style="width: 16px; height: 16px;">
|
|
|
+ <span>新增日程</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import CalendarDateSelectHeader from './CalendarDateSelectHeader.vue'
|
|
|
+import { ref, computed, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue'
|
|
|
+import addIcon from '@/assets/icon/add.svg'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+
|
|
|
+const emit = defineEmits(['back','addOption'])
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ showCalendarSelector:{
|
|
|
+ type:Boolean,
|
|
|
+ default:true
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 是否有管理员权限
|
|
|
+const hasSchoolManager = computed(() => {
|
|
|
+ return true
|
|
|
+})
|
|
|
+
|
|
|
+const today = new Date()
|
|
|
+const pad = (n: number) => String(n).padStart(2, '0')
|
|
|
+const formatDate = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
|
+const formatPopupDate = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
|
+
|
|
|
+// 当前视图年月
|
|
|
+const currentYear = defineModel('currentYear',{ default: new Date().getFullYear() })
|
|
|
+const currentMonth = defineModel('currentMonth',{ default: new Date().getMonth() + 1 })
|
|
|
+
|
|
|
+// 选中日期
|
|
|
+const selectedDate = ref(new Date(today.getFullYear(), today.getMonth(), today.getDate()))
|
|
|
+
|
|
|
+interface HoverState {
|
|
|
+ dateKey: string
|
|
|
+ event: { id: string; title: string } | null
|
|
|
+ eventIndex: number
|
|
|
+}
|
|
|
+const hoverState = ref<HoverState>({ dateKey: '', event: null, eventIndex: 0 })
|
|
|
+
|
|
|
+// 当前选中的日期 以及日期下的事件
|
|
|
+const cellHoverState = ref<{ dateKey: string; date: Date | null }>({ dateKey: '', date: null })
|
|
|
+
|
|
|
+const clearAllPopups = () => {
|
|
|
+ hoverState.value = { dateKey: '', event: null, eventIndex: 0 }
|
|
|
+ cellHoverState.value = { dateKey: '', date: null }
|
|
|
+}
|
|
|
+
|
|
|
+const selectedEventId = ref<string | null>(null)
|
|
|
+
|
|
|
+const handleCellClick = (date: Date, inCurrentMonth: boolean) => {
|
|
|
+ if (!inCurrentMonth) return
|
|
|
+ selectDate(date)
|
|
|
+ selectedEventId.value = null
|
|
|
+ const dateKey = formatDate(date)
|
|
|
+ console.log('执行到了',cellHoverState.value,dateKey)
|
|
|
+ if (cellHoverState.value.dateKey === dateKey) {
|
|
|
+ clearAllPopups()
|
|
|
+ } else {
|
|
|
+ console.log('执行了')
|
|
|
+ clearAllPopups()
|
|
|
+ //
|
|
|
+ cellHoverState.value = { dateKey, date }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleEventClick = (date: Date, ev: { id: string; title: string }, index: number) => {
|
|
|
+ selectDate(date)
|
|
|
+ const dateKey = formatDate(date)
|
|
|
+ if (hoverState.value.dateKey === dateKey && hoverState.value.event?.id === ev.id) {
|
|
|
+ clearAllPopups()
|
|
|
+ selectedEventId.value = null
|
|
|
+ } else {
|
|
|
+ clearAllPopups()
|
|
|
+ hoverState.value = { dateKey, event: ev, eventIndex: index }
|
|
|
+ selectedEventId.value = ev.id
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 打开创建任务弹窗
|
|
|
+const openAddSchedule = (date: Date) => {
|
|
|
+ emit('addOption',date)
|
|
|
+ const dateStr = formatDate(date)
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
|
|
+
|
|
|
+// 日程数据:以 YYYY-MM-DD 为 key
|
|
|
+// const scheduleEvents = ref<Record<string, { id: string; title: string }[]>>({})
|
|
|
+const scheduleEvents = ref({
|
|
|
+ '2026-09-01':[
|
|
|
+ {
|
|
|
+ id: 'string', title: 'string'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+const calendarRows = computed(() => {
|
|
|
+ const y = currentYear.value
|
|
|
+ const m = currentMonth.value - 1
|
|
|
+ const firstDayOfMonth = new Date(y, m, 1)
|
|
|
+ const startWeekday = firstDayOfMonth.getDay()
|
|
|
+ const daysInMonth = new Date(y, m + 1, 0).getDate()
|
|
|
+ return Math.ceil((startWeekday + daysInMonth) / 7)
|
|
|
+})
|
|
|
+
|
|
|
+const cellHeight = computed(() => {
|
|
|
+ const totalHeight = 630
|
|
|
+ const headerHeight = 50
|
|
|
+ const rows = calendarRows.value
|
|
|
+ return Math.floor((totalHeight - headerHeight) / rows)
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+// 获取当前月所有日程
|
|
|
+const fetchMonthSchedule = async () => {
|
|
|
+ const y = currentYear.value
|
|
|
+ const m = currentMonth.value
|
|
|
+ const startDate = `${y}-${pad(m)}-01`
|
|
|
+ const lastDay = new Date(y, m, 0).getDate()
|
|
|
+ const endDate = `${y}-${pad(m)}-${pad(lastDay)}`
|
|
|
+ try {
|
|
|
+ /*
|
|
|
+ const res: any = await getTimePeriodSchedule({ startDate, endDate })
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ Object.keys(res.data).forEach(dateKey => {
|
|
|
+ const list = res.data[dateKey] || []
|
|
|
+ scheduleEvents.value[dateKey] = list.map((item: any) => ({
|
|
|
+ id: String(item.id),
|
|
|
+ title: item.content || ''
|
|
|
+ }))
|
|
|
+ })
|
|
|
+ }
|
|
|
+ */
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleGlobalClick = (event: MouseEvent) => {
|
|
|
+ const target = event.target as HTMLElement
|
|
|
+ if (!target.closest('.cal_cell') && !target.closest('.event_popup') && !target.closest('.cell_popup')) {
|
|
|
+ clearAllPopups()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchMonthSchedule()
|
|
|
+ document.addEventListener('click', handleGlobalClick)
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ document.removeEventListener('click', handleGlobalClick)
|
|
|
+})
|
|
|
+
|
|
|
+// 当用户通过顶部下拉切换年月时,仅切换视图,不改变选中日期;
|
|
|
+// 选中日期只有在点击日历格子时才会改变,保证"选中态"针对具体的年月日而非日号
|
|
|
+
|
|
|
+// 监听年月变化,重新获取日程数据
|
|
|
+watch([currentYear, currentMonth], () => {
|
|
|
+ fetchMonthSchedule()
|
|
|
+})
|
|
|
+
|
|
|
+// 生成当月日历格子(5 行 × 7 列 = 35 格,避免出现第 6 行)
|
|
|
+const calendarCells = computed(() => {
|
|
|
+ const y = currentYear.value
|
|
|
+ const m = currentMonth.value - 1
|
|
|
+ const firstDayOfMonth = new Date(y, m, 1)
|
|
|
+ const startWeekday = firstDayOfMonth.getDay() // 0 - 6
|
|
|
+ const daysInMonth = new Date(y, m + 1, 0).getDate()
|
|
|
+
|
|
|
+ const rowsNeeded = Math.ceil((startWeekday + daysInMonth) / 7)
|
|
|
+ const totalCells = rowsNeeded * 7
|
|
|
+
|
|
|
+ const gridStart = new Date(y, m, 1 - startWeekday)
|
|
|
+
|
|
|
+ const cells = []
|
|
|
+ for (let i = 0; i < totalCells; i++) {
|
|
|
+ const d = new Date(gridStart)
|
|
|
+ d.setDate(gridStart.getDate() + i)
|
|
|
+ const key = formatDate(d)
|
|
|
+ const events = scheduleEvents.value[key] || []
|
|
|
+ cells.push({
|
|
|
+ num: d.getDate(),
|
|
|
+ weekday: d.getDay(),
|
|
|
+ inCurrentMonth: d.getMonth() === m,
|
|
|
+ isToday: d.toDateString() === today.toDateString(),
|
|
|
+ isSelected: d.toDateString() === selectedDate.value.toDateString(),
|
|
|
+ isLastRow: i >= totalCells - 7,
|
|
|
+ events,
|
|
|
+ date: d
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return cells
|
|
|
+})
|
|
|
+
|
|
|
+const selectDate = (d: Date) => {
|
|
|
+ selectedDate.value = d
|
|
|
+ currentYear.value = d.getFullYear()
|
|
|
+ currentMonth.value = d.getMonth() + 1
|
|
|
+}
|
|
|
+
|
|
|
+// 编辑事件
|
|
|
+const editEvent = (ev: { id: string; title: string }) => {
|
|
|
+ emit('addOption')
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// 删除事件
|
|
|
+const deleteEvent = async (ev: { id: string; title: string }) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `是否删除:${ev.title}`,
|
|
|
+ '提示',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ )
|
|
|
+ /*
|
|
|
+ const res: any = await delSchedule(ev.id)
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage.success('日程删除成功')
|
|
|
+ const key = hoverState.value.dateKey
|
|
|
+ if (!scheduleEvents.value[key]) return
|
|
|
+ const idx = scheduleEvents.value[key].findIndex(e => e.id === ev.id)
|
|
|
+ if (idx >= 0) {
|
|
|
+ scheduleEvents.value[key].splice(idx, 1)
|
|
|
+ if (scheduleEvents.value[key].length === 0) delete scheduleEvents.value[key]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ */
|
|
|
+ } catch (err) {
|
|
|
+ if (err !== 'cancel') {
|
|
|
+ console.error(err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /* ===== 日历主体 ===== */
|
|
|
+/* 统一日历网格:星期表头 + 日期格共用同一 grid 容器,保证列宽/竖线完全对齐 */
|
|
|
+ .cal_body {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(7, 1fr);
|
|
|
+ border: 1px solid #EBEEF5;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #fff;
|
|
|
+ overflow: visible;
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ /* 网格整体作为相对容器,允许内部 absolute 元素延伸到外部 */
|
|
|
+
|
|
|
+
|
|
|
+ /* 星期表头单元格:占据第一行,单独的高度/背景/字号 */
|
|
|
+ .cal_header {
|
|
|
+ height: 50px;
|
|
|
+ line-height: 50px;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+ background: #F4F6F8;
|
|
|
+ border-right: 1px solid #EBEEF5;
|
|
|
+ border-bottom: 1px solid #EBEEF5;
|
|
|
+
|
|
|
+ &:nth-child(7) {
|
|
|
+ border-right: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is_weekend {
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /* 日期格 */
|
|
|
+ .cal_cell {
|
|
|
+ background: #fff;
|
|
|
+ padding: 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ transition: background-color 0.15s;
|
|
|
+ border-left: 1px solid transparent;
|
|
|
+ border-top: 1px solid transparent;
|
|
|
+ border-right: 1px solid #EBEEF5;
|
|
|
+ border-bottom: 1px solid #EBEEF5;
|
|
|
+ position: relative;
|
|
|
+ overflow: visible;
|
|
|
+
|
|
|
+ /* 每行最后一列去掉右侧边框 */
|
|
|
+ &:nth-child(7n) {
|
|
|
+ border-right: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 最后一行去掉底部边框(表头算 1-7,日期区从 8 开始;按 grid 总子元素判断) */
|
|
|
+ &:last-child,
|
|
|
+ &:nth-last-child(-n+7) {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is_other_month {
|
|
|
+ background: #fafbfd;
|
|
|
+ color: #bbb;
|
|
|
+
|
|
|
+ .cal_cell_num {
|
|
|
+ color: #666666;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cal_cell_count {
|
|
|
+ color: #666666;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cal_event_text {
|
|
|
+ color: #666666;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is_selected {
|
|
|
+ background: rgba(46, 100, 250, 0.1);
|
|
|
+ border: 1px solid #2E64FA;
|
|
|
+
|
|
|
+ .cal_cell_top {
|
|
|
+ .cal_cell_num {
|
|
|
+ color: #2E64FA;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is_today {
|
|
|
+ .cal_cell_num {
|
|
|
+ color: #2E64FA;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is_weekend:not(.is_other_month) {
|
|
|
+ background: #fafbff;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 顶部:日期 + 数量 */
|
|
|
+ .cal_cell_top {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 8px;
|
|
|
+
|
|
|
+ .cal_cell_num {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 20px;
|
|
|
+ color: #333333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cal_cell_count {
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666666;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /* 事件列表 */
|
|
|
+ .cal_cell_events {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ max-height: 96px;
|
|
|
+ overflow-y: auto;
|
|
|
+ scrollbar-width: none;
|
|
|
+
|
|
|
+ &::-webkit-scrollbar {
|
|
|
+ display: none;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .cal_event_item {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #666;
|
|
|
+ line-height: 1.4;
|
|
|
+ overflow: visible;
|
|
|
+ z-index: 2;
|
|
|
+ max-width: 200px;
|
|
|
+ // &:hover .event_main,
|
|
|
+ // &.is-active .event_main {
|
|
|
+ // background: #000;
|
|
|
+ // }
|
|
|
+
|
|
|
+ .event_main {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ width: 100%;
|
|
|
+ padding: 3px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ position: relative;
|
|
|
+ &.is-active{
|
|
|
+ color: #2E64FA;
|
|
|
+ }
|
|
|
+ &.is_selected {
|
|
|
+ .cal_event_text {
|
|
|
+ color: #2E64FA;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .cal_event_dot {
|
|
|
+ width: 4px;
|
|
|
+ height: 4px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #2E64FA;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .cal_event_text {
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ flex: 1;
|
|
|
+ color: #666666;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /* 悬浮窗基础:挂在 .cal_cell 下,避免被 cal_cell_events 的 overflow 裁剪 */
|
|
|
+ .event_popup {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ min-width: 240px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #EBEEF5;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.14), 0 2px 4px rgba(0, 0, 0, 0.06);
|
|
|
+ padding: 6px 14px 8px 14px;
|
|
|
+ z-index: 200;
|
|
|
+ opacity: 1;
|
|
|
+ visibility: visible;
|
|
|
+ pointer-events: auto;
|
|
|
+ display: block;
|
|
|
+
|
|
|
+ &.popup_right {
|
|
|
+ left: calc(100% + 8px);
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: -6px;
|
|
|
+ top: 20px;
|
|
|
+ transform: rotate(45deg);
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ background: #fff;
|
|
|
+ border-left: 1px solid #EBEEF5;
|
|
|
+ border-bottom: 1px solid #EBEEF5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.popup_left {
|
|
|
+ right: calc(100% + 8px);
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ right: -6px;
|
|
|
+ top: 20px;
|
|
|
+ transform: rotate(45deg);
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ background: #fff;
|
|
|
+ border-right: 1px solid #EBEEF5;
|
|
|
+ border-top: 1px solid #EBEEF5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.popup_top {
|
|
|
+ &::before {
|
|
|
+ top: auto !important;
|
|
|
+ bottom: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .event_popup_title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.5;
|
|
|
+ padding: 6px 0 4px 0;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+
|
|
|
+ .event_popup_actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ border-top: 1px solid #EBEEF5;
|
|
|
+ padding: 6px 0 2px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .event_popup_date {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .event_actions_right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ .event_btn {
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ transition: background-color 0.15s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(46, 100, 250, 0.08);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .event_edit {
|
|
|
+ color: #2E64FA;
|
|
|
+ }
|
|
|
+ .event_delete {
|
|
|
+ color: #F56C6C;
|
|
|
+ &:hover {
|
|
|
+ background: rgba(245, 108, 108, 0.08);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /* 日期格子悬浮窗 */
|
|
|
+ .cell_popup {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ min-width: 260px;
|
|
|
+ max-width: 320px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #EBEEF5;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.14), 0 2px 4px rgba(0, 0, 0, 0.06);
|
|
|
+
|
|
|
+ z-index: 200;
|
|
|
+ opacity: 1;
|
|
|
+ visibility: visible;
|
|
|
+ pointer-events: auto;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ transition: opacity 0.15s ease, visibility 0.15s ease;
|
|
|
+
|
|
|
+ &.popup_right {
|
|
|
+ left: 100%;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: -6px;
|
|
|
+ top: 20px;
|
|
|
+ transform: rotate(45deg);
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ background: #fff;
|
|
|
+ border-left: 1px solid #EBEEF5;
|
|
|
+ border-bottom: 1px solid #EBEEF5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.popup_left {
|
|
|
+ right: 100%;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ right: -6px;
|
|
|
+ top: 20px;
|
|
|
+ transform: rotate(45deg);
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ background: #fff;
|
|
|
+ border-right: 1px solid #EBEEF5;
|
|
|
+ border-top: 1px solid #EBEEF5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.popup_top {
|
|
|
+ top: auto !important;
|
|
|
+ bottom: 20px;
|
|
|
+ &::before {
|
|
|
+ top: auto !important;
|
|
|
+ bottom: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .cell_popup_header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px;
|
|
|
+ .cell_popup_date {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cell_popup_count {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .cell_popup_events {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ max-height: 150px;
|
|
|
+ overflow-y: auto;
|
|
|
+ scrollbar-width: none;
|
|
|
+ padding:0 8px 12px 8px;
|
|
|
+ // &::-webkit-scrollbar {
|
|
|
+ // display: b;
|
|
|
+ // width: 2px;
|
|
|
+ // height: 2px;
|
|
|
+ // }
|
|
|
+
|
|
|
+ .cell_popup_event {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666666;
|
|
|
+ line-height: 1.5;
|
|
|
+
|
|
|
+ .cell_popup_dot {
|
|
|
+ width: 4px;
|
|
|
+ height: 4px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #2E64FA;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-top: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cell_popup_text {
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .cell_popup_empty {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #999999;
|
|
|
+ text-align: center;
|
|
|
+ padding: 16px 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .cell_popup_add {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 6px;
|
|
|
+ padding: 13px 20px;
|
|
|
+ border-top: 1px solid #EBEEF5;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #2E64FA;
|
|
|
+ font-weight: 500;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.15s;
|
|
|
+
|
|
|
+ // &:hover {
|
|
|
+ // background: rgba(46, 100, 250, 0.08);
|
|
|
+ // border-radius: 4px;
|
|
|
+ // }
|
|
|
+
|
|
|
+ :deep(.el-icon) {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #2E64FA;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+</style>
|