lm пре 1 месец
родитељ
комит
e104689028

+ 137 - 0
src/components/CalendarTodoList/CalendarDateSelectHeader.vue

@@ -0,0 +1,137 @@
+<template>
+    <div class="cal_nav">
+        <div class="nav_btn" @click="prevMonth">
+            <el-icon> <ArrowLeft /></el-icon>
+        </div>
+        <template v-if="showDatePicker">
+            <el-date-picker ref="datePickerRef" v-model="datePickerValue" type="month" format="YYYY-MM"
+                value-format="YYYY-MM" @change="onMonthChange" @visible-change="onDatePickerVisibleChange" />
+        </template>
+        <div v-else class="nav_text" @click="openDatePicker">{{ currentYear }}-{{
+            String(currentMonth).padStart(2, '0') }}</div>
+        <div class="nav_btn" @click="nextMonth">
+            <el-icon><ArrowRight /></el-icon>
+        </div>
+    </div>
+</template>
+
+
+<script setup lang="ts">
+import { ref,nextTick } from 'vue'
+
+// 当前视图年月
+const currentYear = defineModel('currentYear')
+const currentMonth = defineModel('currentMonth')
+
+
+
+/**
+ * 处理日期的选择
+ */
+// 年月选择器
+const pad = (n: number|any) => String(n).padStart(2, '0')
+const showDatePicker = ref(false)
+const datePickerValue = ref(`${currentYear.value}-${pad(currentMonth.value)}`)
+const datePickerRef = ref<any>(null)
+
+// 打开日期选择器并展开下拉
+const openDatePicker = () => {
+    showDatePicker.value = true
+    nextTick(() => {
+        if (datePickerRef.value) {
+            datePickerRef.value.focus()
+            datePickerRef.value.togglePicker()
+        }
+    })
+}
+
+// 年月选择器变更
+const onMonthChange = (val: string) => {
+    if (val) {
+        const [year, month] = val.split('-').map(Number)
+        currentYear.value = year
+        currentMonth.value = month
+        datePickerValue.value = val
+    }
+}
+
+// 日期选择器显示/隐藏
+const onDatePickerVisibleChange = (visible: boolean) => {
+    if (!visible) {
+        showDatePicker.value = false
+    }
+}
+
+const prevMonth = () => {
+    const y = currentYear.value
+    const m = currentMonth.value
+    if (m === 1) {
+        currentYear.value = y - 1
+        currentMonth.value = 12
+    } else {
+        currentMonth.value = m - 1
+    }
+}
+
+const nextMonth = () => {
+    const y = currentYear.value
+    const m = currentMonth.value
+    if (m === 12) {
+        currentYear.value = y + 1
+        currentMonth.value = 1
+    } else {
+        currentMonth.value = m + 1
+    }
+}
+
+</script>
+
+
+
+<style lang="scss" scoped>
+
+
+ .cal_nav {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 20px;
+    font-weight: 500;
+    font-size: 20px;
+    color: #333333;
+
+    .nav_btn {
+        width: 24px;
+        height: 24px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border: 1px solid #C0C4CC;
+        border-radius: 50%;
+        cursor: pointer;
+
+        :deep(.el-icon) {
+            color: #666;
+            font-size: 14px;
+        }
+
+        &:hover {
+            border-color: #2E64FA;
+
+            :deep(.el-icon) {
+                color: #2E64FA;
+            }
+        }
+    }
+
+    .nav_text {
+        font-size: 20px;
+        font-weight: 500;
+        color: #333333;
+        text-align: center;
+        cursor: pointer;
+    }
+}
+
+</style>

+ 793 - 0
src/components/CalendarTodoList/index.vue

@@ -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>

+ 3 - 2
src/views/appointmentAndTodo/appointmentRegister/index.vue

@@ -29,10 +29,9 @@
                     </FollowCard>
                     </FollowCard>
                 </template>
                 </template>
             </TimeNavLine>
             </TimeNavLine>
-    </div>
       
       
     <!-- 查看预约详情弹窗 -->
     <!-- 查看预约详情弹窗 -->
-        <FormDialog :defaultFormData="appointmentDefaultFormData" 
+    <FormDialog :defaultFormData="appointmentDefaultFormData" 
             title="查看详情" :submitLoading="false" 
             title="查看详情" :submitLoading="false" 
             :formItemList="appointmentFormItemList" 
             :formItemList="appointmentFormItemList" 
             :visible="appointmentDialogVisible"
             :visible="appointmentDialogVisible"
@@ -55,6 +54,8 @@
             </div>
             </div>
         </template>
         </template>
     </FormDialog>
     </FormDialog>
+
+    </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">

+ 48 - 21
src/views/appointmentAndTodo/index.vue

@@ -26,30 +26,40 @@
              </div>
              </div>
         </div>
         </div>
 
 
-        <div class="page_item appointment_todo">
-            <div class="page_search" style="margin-bottom: 16px">
-                <div class="search_content">
-                    <div class="content_left">
-                        <TabSearch  tabType="box" 
-                            :tabList="tabSearchList" 
-                            :defaultTabKey="defaultTabType"
-                            @tabChange="HandleTypeChange">
-                        </TabSearch>
-                        <FormSearchGroup v-if="defaultTabType === 'appointment'" :searchList="formSearchList" @searchChange=""></FormSearchGroup>
-                        
-                        <!-- 日历导航 -->
-                        <div v-if="defaultTabType === 'todo'">
-                        
-                            日历导航
-                        
+        <div class="detail_content" style="flex:1 1">
+            <div class="page_item appointment_todo">
+                <div class="page_search">
+                    <div class="search_content">
+                        <div class="content_left">
+                            <TabSearch  tabType="box" 
+                                :tabList="tabSearchList" 
+                                :defaultTabKey="defaultTabType"
+                                @tabChange="HandleTypeChange">
+                            </TabSearch>
+                            <FormSearchGroup v-if="defaultTabType === 'appointment'" :searchList="formSearchList" @searchChange=""></FormSearchGroup>
+                          
+                        </div>
+                        <div class="content_right" style="flex:1 1">
+                            <!-- 日历导航 头-->
+                            <CalendarDateSelectHeader 
+                                v-if="defaultTabType === 'todo'"
+                                v-model:currentYear="currentYear"
+                                v-model:currentMonth="currentMonth"
+                            ></CalendarDateSelectHeader>
                         </div>
                         </div>
-
                     </div>
                     </div>
                 </div>
                 </div>
+                <!-- 预约登记 -->
+                <AppointmentRegister  style="margin-top: 16px" v-if="defaultTabType === 'appointment'"></AppointmentRegister>
             </div>
             </div>
-            <AppointmentRegister v-if="defaultTabType === 'appointment'"></AppointmentRegister>
 
 
-            <TodoRegister v-if="defaultTabType === 'todo'"></TodoRegister>
+            <!-- 待办登记 -->
+            <div class="page_item"  style="padding:10px" v-if="defaultTabType === 'todo'">
+                <TodoRegister 
+                    v-model:currentYear="currentYear"
+                    v-model:currentMonth="currentMonth"
+                ></TodoRegister>
+            </div>
         </div>
         </div>
     </div>
     </div>
 </template>
 </template>
@@ -65,11 +75,10 @@ import Calendar from '@/baseComponents/Calendar.vue'
 import AppointmentRegister from './appointmentRegister/index.vue'
 import AppointmentRegister from './appointmentRegister/index.vue'
 
 
 import TodoRegister from './toDoRegister/index.vue'
 import TodoRegister from './toDoRegister/index.vue'
-
+import CalendarDateSelectHeader from '@/components/CalendarTodoList/CalendarDateSelectHeader.vue';
 
 
 const defaultTabType = ref('appointment')
 const defaultTabType = ref('appointment')
 
 
-
 const HandleTypeChange = (val)=>{
 const HandleTypeChange = (val)=>{
     defaultTabType.value = val
     defaultTabType.value = val
 }
 }
@@ -77,6 +86,13 @@ const HandleTypeChange = (val)=>{
 // 当前选中的日历信息
 // 当前选中的日历信息
 const selectedCalendarTime =ref()
 const selectedCalendarTime =ref()
 
 
+const currentYear = ref(new Date().getFullYear())
+const currentMonth = ref(new Date().getMonth() + 1 )
+
+/**
+ * 处理日期的选择
+ */
+
 </script>
 </script>
 
 
 
 
@@ -149,5 +165,16 @@ const selectedCalendarTime =ref()
             }
             }
         }
         }
     }
     }
+
+    .detail_content{
+        flex:1 1;
+        display: flex;
+        flex-direction: column;
+        gap:10px;
+        .appointment_todo{
+            padding:16px;
+        }
+    }
 }
 }
+
 </style>
 </style>

+ 22 - 0
src/views/appointmentAndTodo/toDoRegister/formDialog.ts

@@ -0,0 +1,22 @@
+import { ref } from "vue";
+
+
+// 添加待办表单项列表
+export const todoTaskFormItemList = ref([
+    {
+        key: 'scheduleDetails',
+        label: '待办内容',
+        type: 'textarea',
+        rows:5,
+        placeholder:'请输入日期内容',
+        style: {  }
+    },
+    {
+        key: 'date',
+        label: '待办时间',
+        type: 'dateTimePickerNoRange',
+        placeholder:'请选择',
+        style: { }
+    },
+    
+])

+ 36 - 21
src/views/appointmentAndTodo/toDoRegister/index.vue

@@ -2,22 +2,21 @@
     <div>
     <div>
         <SearchBlock :searchList="blockSearchList"></SearchBlock>
         <SearchBlock :searchList="blockSearchList"></SearchBlock>
 
 
-        <el-calendar  v-model="selectedCalendarTime"  ref="calendar">
-            <!-- 自定义日历头 -->
-            <template #header="{ date }">
-                <i></i>
-            </template>
-
-            <!-- 自定义日期单元格 -->
-            <template #date-cell="{ data}">
-                <span class="has_things">
-                    {{ data.day.split('-').pop() }}
-                </span>
-            </template>
-        </el-calendar>
-
-
-
+        <div class="page_jg_10"></div>
+        <CalendarTodoList 
+            :showCalendarSelector="false"
+            v-model:currentYear="currentYear"
+            v-model:currentMonth="currentMonth"
+            @addOption="OpenTodoDialog" >
+        </CalendarTodoList>
+      
+        <FormDialog title="添加待办" :visible="todoDialogVisible" 
+            :submitLoading="todoDialogSubmitloading" 
+            :defaultFormData="todoDialogDefaultFormData"
+            :formItemList="todoTaskFormItemList"
+            @close="CloseTodoDialog"
+            @confirm="HandleTodoDialogConfirm">
+        </FormDialog>
     </div>
     </div>
 </template>   
 </template>   
 
 
@@ -28,12 +27,28 @@ import { ref } from 'vue'
 import SearchBlock from '@/baseComponents/SearchBlock.vue';
 import SearchBlock from '@/baseComponents/SearchBlock.vue';
 import {blockSearchList} from './searchBlock'
 import {blockSearchList} from './searchBlock'
 
 
+import CalendarTodoList from '@/components/CalendarTodoList/index.vue';
+import FormDialog from '@/baseComponents/FormDialog.vue';
+
+import {todoTaskFormItemList} from './formDialog'
 
 
-const selectedCalendarTime = defineModel('selectedCalendarTime')
-const calendar = ref()
-const selectDate = (val) => {
-  if (!calendar.value) return
-  calendar.value.selectDate(val)
+
+const currentYear = defineModel('currentYear')
+const currentMonth = defineModel('currentMonth')
+
+
+// 创建待办日程弹窗
+const todoDialogVisible = ref(false)
+const todoDialogSubmitloading = ref(false)
+const todoDialogDefaultFormData = ref({})
+const OpenTodoDialog =()=>{
+    todoDialogVisible.value = true
+}
+const CloseTodoDialog =()=>{
+    todoDialogVisible.value = false
+}
+const HandleTodoDialogConfirm =()=>{
+    CloseTodoDialog()
 }
 }
 </script>
 </script>