vue3 中 element-plus Calendar 实现自定义日历头部,同时可选择日期,加入拖拽事件以及如何修改每周第一天

效果图:

image.png

版本与依赖:注意这里引入了webpack-merge

image.png

大概思路如下

1.修改日历周一至周五的文本:

解决办法:使用Config Provider

2.自定义日历头部:

首先element-plus官网提供了demo,但是我未使用这种方式,经测试发现这样不能通过顶部的日期组件选择日期

image.png

解决办法:使用dayjs自己实现日期快捷切换,这里把这部分逻辑抽离到myCalendar.ts中

image.png

注意Element-plus (opens new window)组件库默认支持 dayjs 进行日期时间处理,所以可以直接导入使用。

3.使用date-cell 的 scoped-slot 来自定义日历单元格,从而接入拖拽事件

4.使用 data.type === 'current-month'实现只显示当月日期

5.若你的element-plus版本使用#date-cell不能实现自定义日历单元格,可改为#dateCell试下

完整代码如下:

index.vue

<template>
  <div class="panel-content">
    <ELPlusLanguageConfig :local="local">
            <el-calendar :model-value="new Date(currentMonth)">
              <template #header="{ date }">

                <div class="button-box">
                  <div class="control-list">
                    <el-button round size="small" @click="selectDate('prev-year')">上一年</el-button>
                    <el-button round size="small" @click="selectDate('prev-month')">上一月</el-button>
                    <el-date-picker class="date-picker" v-model="currentMonth" :value-format="valueFormat" type="month"
                      placeholder="选择月份" />
                    <el-button round size="small" @click="selectDate('next-month')">下一月</el-button>
                    <el-button round size="small" @click="selectDate('next-year')">下一年</el-button>
                    <el-button round size="small" @click="selectDate('today')">今天</el-button>
                  </div>
                  <div>
                    <!-- <el-button type="primary" size="small">导入</el-button> -->
                  </div>
                </div>

              </template>
              <template #dateCell="{ data }">
                <div :class="{ 'dayBox': true }">
                  <div v-if="isCurrentMonth(data)" :class="{ 'dargIn': retData.dragIn === data.day }"
                    @click.stop="handleClick(data)" @drop="onDrop" @dragover="$event => onDragover($event, data)">
                    <h1>{{ formatDay(data) }}</h1>
                    <!-- 删除 -->
                    <el-icon class="del" @click.stop="del(data)">
                      <Delete />
                    </el-icon>
                  </div>
                </div>
              </template>
            </el-calendar>
          </ELPlusLanguageConfig>

          <div class="right-box">
            <div class="d-title">选择值班人员</div>
            <div class="content-box">

              <div class="t-one" v-for="(item, index) in retData.people" :key="index">
                <!-- 组 -->
                <div class="draggable" v-if="item.type" :draggable="true" @dragstart="onDragStart(item)">
                  {{ item.name }}
                  <p><span v-for="(sub, subIndex) in item.children" :key="subIndex">{{ sub.name }}</span></p>
                </div>
                <!-- 未分组 -->
                <div v-else>
                  {{ item.name }}
                  <p>
                    <span class="draggable" v-for="(sub, subIndex) in item.children" :key="subIndex" :draggable="true"
                      @dragstart="onDragStart(sub)">{{ sub.name }}</span>
                  </p>
                </div>
              </div>

            </div>

          </div>

  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import ELPlusLanguageConfig from '../components/ELPlusLanguageConfig.vue'
import { useCalendar } from '../components/myCalendar'

type calendarDateCell = { type: 'prev-month' | 'current-month' | 'next-month', isSelected: boolean, day: string, date: Date }

// 日历相关 start --------
let valueFormat = 'YYYY-MM'
const { currentMonth, selectDate, local } = useCalendar(valueFormat)

// 日期格式化显示
const formatDay = ({ day }:calendarDateCell) => {
  return Number(day.split('-')[2])
}
// 判断是否是当前月
const isCurrentMonth = (data:calendarDateCell) => {
  return data.type === 'current-month'
}

const handleClick = (data:calendarDateCell) => {
  console.log(data, 'click')
}

// 删除
const del = (data:calendarDateCell) => {
  console.log(data, 'del')
}

// 日历相关 end --------

// 拖拽相关 start ------- 

const retData = reactive({
  people: [
    {
      type: 'group',
      name: '组1',
      children: [
        {
          name: '李xx'
        },
        {
          name: '李bbb'
        }
      ]
    },
    {
      type: 'group',
      name: '组2',
      children: [
        {
          name: '李xx2'
        },
        {
          name: '李xxx3'
        }
      ]
    },
    {
      name: '未分配人员',
      children: [
        {
          name: '张三'
        },
        {
          name: '李四'
        }
      ]
    }
  ],
  dragIn: null, // 拖入的盒子,设置样式使用
  dragData: {} // 拖入的数据
})

const onDragStart = (data:any) => {
  console.log('onDragStart', data)
  retData.dragData = data
}

const onDragover = (e:DragEvent, data:any) => {
  // console.log('onDragover', e)
  e.preventDefault();
  retData.dragIn = data.day
}
const onDrop = (ev:DragEvent) => {
  // console.log('onDrop', ev)
  console.log('onDrop', retData.dragIn, retData.dragData)
  retData.dragIn = null
}

// 拖拽相关 end -------

</script>

<style lang="less" scoped>

.panel-content {
  background-color: #122645;
  display: flex;
  justify-content: space-between;
  align-items: stretch;
  height: 100%;
  color: #ffffff;

  .button-box {
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .control-list {
      display: flex;
      justify-content: flex-start;
      align-items: center;

      button {
        height: 34px;
        width: 88px;
        font-size: 16px;
        border: none;
        transition: all .3s;
      }

      :deep(.date-picker) {
        width: 188px;
        height: 40px;
        margin: 0 15px;
      }
    }


  }

  .dayBox {
    position: relative;
    height: 100%;
    width: 100%;
    font-size: 30px;
    font-family: MicrosoftYaHei-Bold-, MicrosoftYaHei-Bold;
    cursor: default;

    >div {
      height: 100%;
    }

    h1 {
      padding: 10px 16px 0 16px;
      font-size: 30px;
      margin: 0;
    }

    .del {
      cursor: pointer;
      position: absolute;
      bottom: 9px;
      right: 9px;
      font-size: 16px;

      &:hover {
        color: #0d84ff;
      }
    }
  }

  .right-box {
    width: 729px;
    flex: none;
    background-color: rgba(45, 103, 254, 0.12);
    margin-top: 64px;

    .d-title {
      background-color: #1C4294;
      font-size: 18px;
      padding: 16px 38px;
    }

    @PADDING: 15px;

    .content-box {
      padding: 0 (38px - @PADDING);
      overflow-y: auto;
      box-sizing: border-box;
      max-height: calc(100vh - 300px);
      ;
    }

    .t-one {
      margin-top: 44px - 2*@PADDING;

      &:first-child {
        margin-top: 44px - @PADDING;
      }

      >div {
        padding: @PADDING;
      }


      p {
        display: flex;
        justify-content: flex-start;
        flex-wrap: wrap;
        flex-direction: row;
        margin: 0;
      }

      span {
        background-color: #122645;
        width: 138px;
        height: 40px;
        line-height: 40px;
        padding: 0 16px;
        border: 1px solid #196BD1;
        margin-right: 13px;
        margin-top: 17px;
      }
    }
  }
}

.draggable {
  cursor: move;

  &:hover {
    outline: 1px dashed #196BD1;
    box-shadow: 0px 0px 20px 1px rgb(25, 107, 209,0.5);
  }
}

.dargIn {
  outline: 1px dashed #196BD1;
}
</style>

<!-- 全局样式 -->
<style lang="less">
@themeColor:#ffffff;
.el-calendar {
  background-color: transparent;

  .el-calendar__header {
    margin-bottom: 0;
    padding-bottom: 0;
  }

  .el-calendar__body {
    padding-top: 5px;
    padding-bottom: 0;
  }

  .el-calendar-day {
    padding: 0;
    // border: 1px solid #b4bbc5;
    border-radius: 4px;
    height: 126px;
    background-color: #081827;
  }

  // 其他月隐藏
  // .el-calendar-table:not(.is-range) td.prev,
  // .el-calendar-table:not(.is-range) td.next {
  //   color: transparent;
  // }

  // 顶部周一、周日
  .el-calendar-table thead {
    background-color: #061529;

    th {
      font-weight: normal;
      color: @themeColor;
      font-size: 18px;
      padding: 10px 0;
    }
  }

  .el-calendar-table tr td:first-child {
    border-left: none;
  }

  .el-calendar-table tr td:last-child {
    border-right: none;
  }

  .el-calendar-table td.is-today {
    color: @themeColor;

    .el-calendar-day {
      background-color: rgba(45, 103, 254, 0.35);
    }
  }

  .el-calendar-table__row:last-child {
    td {
      border-bottom: none;
    }
  }

  // hover
  .el-calendar-table .el-calendar-day:hover {
    // .dayBox{
    //   background-color: rgba(102, 177, 255,0.5);
    // }
  }

  // 选中项设置
  .el-calendar-table td.is-selected {
    background-color: transparent;

    .el-calendar-day {
      border-color: #0d84ff;
    }
  }
}

</style>

ELPlusLanguageConfig.vue

<!-- 给elementplus 单独设置国际化内容 -->
<template>
    <el-config-provider :locale="locale">
        <slot></slot>
    </el-config-provider>
</template>

<script setup lang="ts">

// import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// import en from 'element-plus/dist/locale/en.mjs'

import zhCn from 'element-plus/lib/locale/lang/zh-cn';
// import en from 'element-plus/lib/locale/lang/en';
import { merge } from 'webpack-merge'; 
import {reactive,watch,ref} from 'vue'

const props = defineProps({
    local: { // 国际化
        type: Object,
        default() {
            return {}
        }
    }
})
const locale = ref({})
watch(()=>props.local,(newVal,oldVal)=>{
    locale.value = merge({}, zhCn, newVal)
},{
    immediate:true
})
</script>

<style scoped></style>

myCalendar.ts

import { ref, reactive } from 'vue'
import {dayjs} from "element-plus"
/**
 * 日历切换,快捷时间切换改为结合dayjs实现
 * @param {*} valueFormat 
 * @returns 
 */
export function useCalendar(valueFormat = 'YYYY-MM') {
    // 修改element-plus日历文案
    const local = {
        el: {
            datepicker: {
                weeks: {
                    fri: "周五",
                    mon: "周一",
                    sat: "周六",
                    sun: "周日",
                    thu: "周四",
                    tue: "周二",
                    wed: "周三",
                }
            }
        }
    }

    // 日期
    const currentMonth = ref(dayjs(new Date()).format(valueFormat))
    // 这里没有使用element-plus案例中的方式【因为没有同时生效】,这里改为自己实现快捷切换
    const selectDate = (val:'prev-year'|'prev-month'|'next-month'|'next-year'|'today') => {
        let ploy = {
            // 上一年
            'prev-year': () => {
                currentMonth.value = dayjs(currentMonth.value).add(-1, 'year').format(valueFormat)
            },
            // 上一月
            'prev-month': () => {
                currentMonth.value = dayjs(currentMonth.value).add(-1, 'month').format(valueFormat)
            },
            // 下一月
            'next-month': () => {
                currentMonth.value = dayjs(currentMonth.value).add(1, 'month').format(valueFormat)
            },
            // 下一年
            'next-year': () => {
                currentMonth.value = dayjs(currentMonth.value).add(1, 'year').format(valueFormat)
            },
            // 今天
            'today': () => {
                currentMonth.value = dayjs(new Date()).format(valueFormat)
            },
        }

        ploy[val]()
    }
    return {
        local,
        currentMonth, // 日历组件上的modelValue值,也是日期组件的-model值
        selectDate // 快捷切换时间的事件
    }
}

补充:如何更改每周第一天?

默认一周是周日开始的,想从周六或者周一开始怎么办?

1.查看elementplus文档,提到配置国际化可解决此问题,但是试了下没成功。。。

image.png

2.查看elementplus issue发现此问题在之前已经有人问过了 :

https://github.com/element-plus/element-plus/issues?q=weekStart

image.png

里面提到两种解决办法,第一种还是国际化配置的方式,第二种设置dayjs.en.weekStart
image.png

其中方式二代码如下:
image.png

我试了下方式二确实可以,方式一未成功,有需要的小伙伴自行尝试吧!
此问题参考:
elementplus 配置日期控件el-date-picker的周次从周一开始
elementplus日期选择器周选择从周一开始
element-plus github issue

若对你有帮助,请点个赞吧,若能打赏不胜感激,谢谢支持!
本文地址:https://www.jianshu.com/p/ac88cabc0af5?v=1680746028292,转载请注明出处,谢谢。

参考:
element-plus日历组件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270