• vue可ctrl、shift多选,可添加标记的日历组件


    需要写一个日历组件有以下功能,element无法满足,自己造一个

    • 只显示当前月日期,前后月日期不显示也不可选择;
    • 周六、周日颜色与工作日区分
    • 日期可以打标记(左上角标记)
    • 可ctrl+shift+鼠标左键多选

    目录

    一、 按照 "日", "一", "二", "三", "四", "五", "六" 把一个月的日期排列

    二、单元格样式处理

    三、单机、按住ctrl点击、按住shift点击事件处理

    1.记录键盘按下ctrl、shift事件

    2.点击事件处理

    3.遵循excel点击的操作方式:

    四、组件代码:


    一、 按照 "日", "一", "二", "三", "四", "五", "六" 把一个月的日期排列

    页面

    1. <template>
    2. <div class="calendar">
    3. <div class="calendar_header">{{currentMonth}}月</div>
    4. <table cellspacing="0" cellpadding="0" class="calendar_table">
    5. <thead>
    6. <th v-for="day in WEEK_DAYS" :key="day" :class="['thead_th',day=='六'||day=='日'?'thead_th_red':'']">
    7. {{day}}</th>
    8. </thead>
    9. <tbody>
    10. <tr v-for="(row,index) in rows" :key="index">
    11. <td v-for="(cell,key) in row" :key="key" class="td" @click="handlePickDay(cell)">
    12. <div :class="getCellClass(cell,key)">
    13. <div v-if="cell.type=='current'" class="triangle" :style="getCelltriangleStyle(cell)"></div>
    14. <span class="cell_text">{{cell.text==0?'':cell.text}}</span>
    15. </div>
    16. </td>
    17. </tr>
    18. </tbody>
    19. </table>
    20. </div>
    21. </template>

    组件定义props、emits

    1. const props = defineProps({
    2. markList: {//标记
    3. type: Array<any>,
    4. default: (): any[] => {
    5. return [];
    6. },
    7. },
    8. month: {
    9. type: Date,
    10. required: true,
    11. },
    12. disabled: {
    13. type: Boolean,
    14. default: false,
    15. },
    16. disableBefore: {//禁用今天之前的日期
    17. type: Boolean,
    18. default: true,
    19. },
    20. });
    21. const emits = defineEmits(['change']);

    当前月的日期数组

    1. type CalendarDateCellType = 'next' | 'prev' | 'current'
    2. type CalendarDateCell = {
    3. text: number,
    4. type: CalendarDateCellType
    5. }
    6. const WEEK_DAYS = ref(["日", "一", "二", "三", "四", "五", "六"]);
    7. const currentMonth = computed(() => {
    8. return moment(props.month).format('M');
    9. })
    10. onMounted(() => {
    11. onKeyEvent();
    12. })
    13. const rows = computed(() => {
    14. let days: CalendarDateCell[] = []
    15. const firstDay = moment(props.month).startOf("month").date();
    16. // const endDay = moment(props.month).endOf('month').date();//当前月的最后一天
    17. const firstDayOfWeek = moment(props.month).startOf("month").day();
    18. // const daysInMonth = moment(props.month).daysInMonth();//当前月的天数
    19. const prevMonthDays: CalendarDateCell[] = getPrevMonthLastDays(
    20. firstDay,
    21. firstDayOfWeek - firstDay
    22. ).map((day) => ({
    23. text: 0,//上月补0
    24. type: 'prev',
    25. }))
    26. const currentMonthDays: CalendarDateCell[] = getMonthDays(moment(props.month).daysInMonth()).map(
    27. (day) => ({
    28. text: day,
    29. type: 'current',
    30. })
    31. )
    32. days = [...prevMonthDays, ...currentMonthDays]
    33. const remaining = 7 - (days.length % 7 || 7)
    34. const nextMonthDays: CalendarDateCell[] = rangeArr(remaining).map(
    35. (_, index) => ({
    36. text: 0,//下月补0
    37. type: 'next',
    38. })
    39. )
    40. days = days.concat(nextMonthDays)
    41. // console.log(currentMonth.value, firstDay, endDay, moment(props.month).startOf("month").day());
    42. return toNestedArr(days)
    43. })

    二、单元格样式处理

    • 禁用
    • 选中
    • 周六、周日红色字体
    1. const getCellClass = (cell: CalendarDateCell, key: number) => {
    2. let date = getCellDate(cell);
    3. if (props.disableBefore && date.getTime() < new Date().getTime()) {
    4. return ['cell', 'cell_disabled'];//禁用
    5. }
    6. let classes: string[] = ['cell', 'cell_enabled'];
    7. if (key == 0 || key == 6) {//周六、周日
    8. classes.push('cell_red');
    9. }
    10. let index = selectList.value.indexOf(moment(date).format('YYYY-MM-DD'));
    11. if (index != -1) {
    12. classes.push('cell_active');//选中
    13. }
    14. return classes;
    15. }
    • 左上角三角形标记
    1. .triangle {
    2. position: absolute;
    3. left: 0;
    4. top: 0;
    5. width: 0;
    6. height: 0;
    7. // border-top: 30px solid #e6e911;
    8. border-right: 30px solid transparent;
    9. }
    1. const getCelltriangleStyle = (cell: CalendarDateCell) => {
    2. let date = getCellDate(cell);
    3. let day = props.markList.find(item => item.date == moment(date).format('YYYY-MM-DD'));
    4. return day ? {
    5. borderTop: '30px solid #e6e911',
    6. } : {};
    7. }

    三、单机、按住ctrl点击、按住shift点击事件处理

    1.记录键盘按下ctrl、shift事件

    1. const isCtrl = ref(false)
    2. const isShift = ref(false)
    3. const onKeyEvent = () => {
    4. window.addEventListener('keydown', e => {
    5. e.preventDefault();//取消默认事件
    6. let e1 = e || window.event
    7. switch (e1.keyCode) {
    8. case 16:
    9. isShift.value = true;
    10. break;
    11. case 17:
    12. isCtrl.value = true;
    13. break;
    14. }
    15. })
    16. window.addEventListener('keyup', e => {
    17. e.preventDefault();
    18. let e1 = e || window.event
    19. switch (e1.keyCode) {
    20. case 16:
    21. isShift.value = false;
    22. break;
    23. case 17:
    24. isCtrl.value = false;
    25. break;
    26. }
    27. })
    28. }

    2.点击事件处理

    1. const selectList = ref<any[]>([]);//已选择的
    2. const shiftNum = ref(0);//shift复制的起始位置
    3. const lastSelect = ref<any[]>([]);//按住shift倒数第二次复制的
    4. //遵循excel点击、ctrl、shift组合操作规范
    5. const handlePickDay = (cell: CalendarDateCell) => {
    6. let date = getCellDate(cell);
    7. if (cell.type != 'current') {
    8. return;
    9. }
    10. if (props.disableBefore && date.getTime() < new Date().getTime()) {
    11. return
    12. }
    13. // console.log(isCtrl.value, isShift.value);
    14. let dateStr = moment(date).format('YYYY-MM-DD');
    15. let currentSelect: string[] = [];
    16. //按住ctrl
    17. if (isCtrl.value) {
    18. if (selectList.value.includes(dateStr)) {
    19. selectList.value.splice(selectList.value.indexOf(dateStr), 1);
    20. } else {
    21. selectList.value.push(dateStr);
    22. }
    23. lastSelect.value = [];
    24. } else if (isShift.value) {//按住shift
    25. if (shiftNum.value == 0) {//无上次点击
    26. shiftNum.value = cell.text;
    27. if (selectList.value.includes(dateStr)) {
    28. selectList.value.splice(selectList.value.indexOf(dateStr), 1);
    29. } else {
    30. selectList.value.push(dateStr);
    31. }
    32. } else {
    33. if (shiftNum.value < cell.text) {
    34. currentSelect = getDatesInRange(shiftNum.value, cell.text);
    35. } else if (shiftNum.value > cell.text) {
    36. currentSelect = getDatesInRange(cell.text, shiftNum.value);
    37. } else {
    38. currentSelect = [dateStr];
    39. }
    40. selectList.value = selectList.value.filter(item => !lastSelect.value.includes(item));//移除上次按shift复制的
    41. selectList.value = selectList.value.concat(currentSelect);//添加本次按shift复制的
    42. lastSelect.value = currentSelect;
    43. }
    44. } else {
    45. selectList.value = [dateStr];
    46. }
    47. if (!isShift.value) {
    48. shiftNum.value = cell.text;
    49. }
    50. selectList.value = [...new Set(selectList.value)].sort();//去重、排序
    51. console.log(shiftNum.value, selectList.value);
    52. emits('change', selectList.value);
    53. }

    3.遵循excel点击的操作方式:

    • 未按ctrl、shift点击=>只选择当前点击的;
    • 按ctrl点击=>未选中则选中,已选中则取消选中;
    • 按住shift点击=>记录shift按下时点击位置-->再次点击时把期间内的选中,并移除倒数第二次的选中(否则都会选中,并在按下ctrl时释放上次选中)

    四、组件代码:

    1. <template>
    2. <div class="calendar">
    3. <div class="calendar_header">{{currentMonth}}月</div>
    4. <table cellspacing="0" cellpadding="0" class="calendar_table">
    5. <thead>
    6. <th v-for="day in WEEK_DAYS" :key="day" :class="['thead_th',day=='六'||day=='日'?'thead_th_red':'']">
    7. {{day}}</th>
    8. </thead>
    9. <tbody>
    10. <tr v-for="(row,index) in rows" :key="index">
    11. <td v-for="(cell,key) in row" :key="key" class="td" @click="handlePickDay(cell)">
    12. <div :class="getCellClass(cell,key)">
    13. <div v-if="cell.type=='current'" class="triangle" :style="getCelltriangleStyle(cell)"></div>
    14. <span class="cell_text">{{cell.text==0?'':cell.text}}</span>
    15. </div>
    16. </td>
    17. </tr>
    18. </tbody>
    19. </table>
    20. </div>
    21. </template>
    22. <script lang="ts" setup>
    23. import { ref, computed, onMounted } from "vue";
    24. import moment from "moment";
    25. const props = defineProps({
    26. markList: {//标记
    27. type: Array<any>,
    28. default: (): any[] => {
    29. return [];
    30. },
    31. },
    32. month: {
    33. type: Date,
    34. required: true,
    35. },
    36. disabled: {
    37. type: Boolean,
    38. default: false,
    39. },
    40. disableBefore: {//禁用今天之前的日期
    41. type: Boolean,
    42. default: true,
    43. },
    44. });
    45. const emits = defineEmits(['change']);
    46. type CalendarDateCellType = 'next' | 'prev' | 'current'
    47. type CalendarDateCell = {
    48. text: number,
    49. type: CalendarDateCellType
    50. }
    51. const WEEK_DAYS = ref(["日", "一", "二", "三", "四", "五", "六"]);
    52. const currentMonth = computed(() => {
    53. return moment(props.month).format('M');
    54. })
    55. onMounted(() => {
    56. onKeyEvent();
    57. })
    58. const rows = computed(() => {
    59. let days: CalendarDateCell[] = []
    60. const firstDay = moment(props.month).startOf("month").date();
    61. // const endDay = moment(props.month).endOf('month').date();//当前月的最后一天
    62. const firstDayOfWeek = moment(props.month).startOf("month").day();
    63. // const daysInMonth = moment(props.month).daysInMonth();//当前月的天数
    64. const prevMonthDays: CalendarDateCell[] = getPrevMonthLastDays(
    65. firstDay,
    66. firstDayOfWeek - firstDay
    67. ).map((day) => ({
    68. text: 0,//上月补0
    69. type: 'prev',
    70. }))
    71. const currentMonthDays: CalendarDateCell[] = getMonthDays(moment(props.month).daysInMonth()).map(
    72. (day) => ({
    73. text: day,
    74. type: 'current',
    75. })
    76. )
    77. days = [...prevMonthDays, ...currentMonthDays]
    78. const remaining = 7 - (days.length % 7 || 7)
    79. const nextMonthDays: CalendarDateCell[] = rangeArr(remaining).map(
    80. (_, index) => ({
    81. text: 0,//下月补0
    82. type: 'next',
    83. })
    84. )
    85. days = days.concat(nextMonthDays)
    86. // console.log(currentMonth.value, firstDay, endDay, moment(props.month).startOf("month").day());
    87. return toNestedArr(days)
    88. })
    89. const rangeArr = (n: number) => {
    90. return Array.from(Array.from({ length: n }).keys())
    91. }
    92. const toNestedArr = (days: CalendarDateCell[]) =>
    93. rangeArr(days.length / 7).map((index) => {
    94. const start = index * 7
    95. return days.slice(start, start + 7)
    96. });
    97. const getPrevMonthLastDays = (lastDay: number, count: number) => {
    98. return rangeArr(count).map((_, index) => lastDay - (count - index - 1))
    99. }
    100. const getMonthDays = (days: number) => {
    101. return rangeArr(days).map((_, index) => index + 1)
    102. }
    103. /**
    104. * 获取范围期间所有日期
    105. * @ return array['YYYY-MM-DD']
    106. */
    107. const getDatesInRange = (start: number, end: number): string[] => {
    108. let list = [];
    109. for (let i = start; i <= end; i++) {
    110. let dateStr = moment(getCellDate({ text: i, type: 'current' })).format('YYYY-MM-DD');
    111. list.push(dateStr);
    112. }
    113. return list;
    114. }
    115. const isCtrl = ref(false)
    116. const isShift = ref(false)
    117. const onKeyEvent = () => {
    118. window.addEventListener('keydown', e => {
    119. e.preventDefault();//取消默认事件
    120. let e1 = e || window.event
    121. switch (e1.keyCode) {
    122. case 16:
    123. isShift.value = true;
    124. break;
    125. case 17:
    126. isCtrl.value = true;
    127. break;
    128. }
    129. })
    130. window.addEventListener('keyup', e => {
    131. e.preventDefault();
    132. let e1 = e || window.event
    133. switch (e1.keyCode) {
    134. case 16:
    135. isShift.value = false;
    136. break;
    137. case 17:
    138. isCtrl.value = false;
    139. break;
    140. }
    141. })
    142. }
    143. const getCellClass = (cell: CalendarDateCell, key: number) => {
    144. let date = getCellDate(cell);
    145. if (props.disableBefore && date.getTime() < new Date().getTime()) {
    146. return ['cell', 'cell_disabled'];//禁用
    147. }
    148. let classes: string[] = ['cell', 'cell_enabled'];
    149. if (key == 0 || key == 6) {//周六、周日
    150. classes.push('cell_red');
    151. }
    152. let index = selectList.value.indexOf(moment(date).format('YYYY-MM-DD'));
    153. if (index != -1) {
    154. classes.push('cell_active');//选中
    155. }
    156. return classes;
    157. }
    158. const getCelltriangleStyle = (cell: CalendarDateCell) => {
    159. let date = getCellDate(cell);
    160. let day = props.markList.find(item => item.date == moment(date).format('YYYY-MM-DD'));
    161. return day ? {
    162. borderTop: '30px solid #e6e911',
    163. } : {};
    164. }
    165. const getCellDate = (cell: CalendarDateCell) => new Date(props.month.getFullYear(), props.month.getMonth(), cell.text)
    166. const selectList = ref<any[]>([]);//已选择的
    167. const shiftNum = ref(0);//shift复制的起始位置
    168. const lastSelect = ref<any[]>([]);//按住shift倒数第二次复制的
    169. //遵循excel点击、ctrl、shift组合操作规范
    170. const handlePickDay = (cell: CalendarDateCell) => {
    171. let date = getCellDate(cell);
    172. if (cell.type != 'current') {
    173. return;
    174. }
    175. if (props.disableBefore && date.getTime() < new Date().getTime()) {
    176. return
    177. }
    178. // console.log(isCtrl.value, isShift.value);
    179. let dateStr = moment(date).format('YYYY-MM-DD');
    180. let currentSelect: string[] = [];
    181. //按住ctrl
    182. if (isCtrl.value) {
    183. if (selectList.value.includes(dateStr)) {
    184. selectList.value.splice(selectList.value.indexOf(dateStr), 1);
    185. } else {
    186. selectList.value.push(dateStr);
    187. }
    188. lastSelect.value = [];
    189. } else if (isShift.value) {//按住shift
    190. if (shiftNum.value == 0) {//无上次点击
    191. shiftNum.value = cell.text;
    192. if (selectList.value.includes(dateStr)) {
    193. selectList.value.splice(selectList.value.indexOf(dateStr), 1);
    194. } else {
    195. selectList.value.push(dateStr);
    196. }
    197. } else {
    198. if (shiftNum.value < cell.text) {
    199. currentSelect = getDatesInRange(shiftNum.value, cell.text);
    200. } else if (shiftNum.value > cell.text) {
    201. currentSelect = getDatesInRange(cell.text, shiftNum.value);
    202. } else {
    203. currentSelect = [dateStr];
    204. }
    205. selectList.value = selectList.value.filter(item => !lastSelect.value.includes(item));//移除上次按shift复制的
    206. selectList.value = selectList.value.concat(currentSelect);//添加本次按shift复制的
    207. lastSelect.value = currentSelect;
    208. }
    209. } else {
    210. selectList.value = [dateStr];
    211. }
    212. if (!isShift.value) {
    213. shiftNum.value = cell.text;
    214. }
    215. selectList.value = [...new Set(selectList.value)].sort();//去重、排序
    216. console.log(shiftNum.value, selectList.value);
    217. emits('change', selectList.value);
    218. }
    219. </script>
    220. <style lang="scss" scoped>
    221. .calendar {
    222. width: 100%;
    223. padding: 12px 20px 35px;
    224. &_header {
    225. display: flex;
    226. justify-content: center;
    227. border: 1px solid var(--el-border-color-lighter);
    228. padding: 12px 20px;
    229. }
    230. &_table {
    231. width: 100%;
    232. .thead_th {
    233. padding: 12px 0;
    234. color: var(--el-text-color-regular);
    235. font-weight: 400;
    236. &_red {
    237. color: red;
    238. }
    239. }
    240. }
    241. .td {
    242. border: 1px solid var(--el-border-color-lighter);
    243. -moz-user-select: none;
    244. /*火狐*/
    245. -webkit-user-select: none;
    246. /*webkit浏览器*/
    247. -ms-user-select: none;
    248. /*IE10*/
    249. -khtml-user-select: none;
    250. /*早期浏览器*/
    251. user-select: none;
    252. }
    253. .cell {
    254. // background-color: #409eff;
    255. position: relative;
    256. text-align: center;
    257. min-height: 50px;
    258. display: flex;
    259. justify-content: center;
    260. &_enabled {
    261. cursor: pointer;
    262. color: #373737
    263. }
    264. &_disabled {
    265. cursor: not-allowed;
    266. color: #9b9da1;
    267. }
    268. &_red {
    269. color: red;
    270. }
    271. &_active {
    272. background-color: #409eff;
    273. }
    274. .triangle {
    275. position: absolute;
    276. left: 0;
    277. top: 0;
    278. width: 0;
    279. height: 0;
    280. // border-top: 30px solid #e6e911;
    281. border-right: 30px solid transparent;
    282. }
    283. &_text {
    284. margin: auto;
    285. }
    286. }
    287. }
    288. </style>

    使用:

    onChange">

  • 相关阅读:
    【从安装JDK开始】Spring Cloud + Apache Ignite简单实例
    PHP简单实现预定义钩子和自定义钩子
    【MindSpore易点通机器人-01】你也许见过很多知识问答机器人,但这个有点不一样
    gslx680触摸屏驱动源码码分析(gslX680.c)
    【JavaWeb】Cookie&Session
    ​电脑上的回收站怎么隐藏 ,怎么隐藏桌面回收站图标
    “UTONMOS”掀起元宇宙游戏热潮,全球发展前景广阔
    Linux8-fork父子进程逻辑地址相同、进程的逻辑地址与物理地址、fork相关例题、僵死进程
    团建游戏----做一只小船去漂流
    【总结】使用livy 提交spark任务时报错Connection refused
  • 原文地址:https://blog.csdn.net/jerry872235631/article/details/126991097