React-周视图组件封装

技术栈:React、antd

一 需求背景

使用周视图来显示广播信息与状态

二 组件特点

  1. 当多个时间块交叠时,并行显示。对于交叠时间块,最多显示3个,如果要显示全部交叠的时间块,可点击展开。
  2. 可对时间段精度进行扩展。当有多个时间段短的时间块相邻但不重叠时,方便看查。
  3. 支持跨天显示。对于跨天的广播,会将其分割。假设该广播跨了3天,那么就会根据日期分割成3个时间块,单独填充到对应的日期。
  4. 支持点击回到本周。

三 效果展示

四 实现过程

4.1 数据转换

本示例的原始数据如下:

json 复制代码
{
	"code":	0,
	"description":	"成功",
	"data":	{
		"list":	[{
				"ebmid":	"24300000000000103010101202409240001",
				"name":	"1122222",
				"status":	"3",
				"start_time":	"2024-09-24 16:30:39",
				"end_time":	"2024-09-24 16:34:32",
				"msg_type":	"1",
				"covered_regions":	"常德市",
				"creator":	"省平台"
			}]
      }
}

最后渲染到组件的数据如下,省略了一些无关属性方便理解:

javascript 复制代码
{
  "2024-09-24": [Array<TimeBlock>]
}

该转换过程给原始元素加了splited_start_time、splited_end_time、key三个属性,后者用于标识时间块的唯一性。

一般情况下,日期对应的值是单个时间块元素就可以了。但是因为存在时间段重叠的原因,就对元素进行了二次分组,将时间重叠的时间块分为一组。

json 复制代码
{
    "2024-09-24": [
        [
            {
                "start_time": "2024-09-24 16:30:39",
                "end_time": "2024-09-24 16:34:32",
                "splited_start_time": "2024-09-24 16:30:39",
                "splited_end_time": "2024-09-24 16:34:32",
                "key": "4FFXJ2CG"
            }
        ]
    ],
    "2024-09-26": [
        [
            {
                "start_time": "2024-09-26 01:16:09",
                "end_time": "2024-09-26 02:20:02",
                "splited_start_time": "2024-09-26 01:16:09",
                "splited_end_time": "2024-09-26 02:20:02",
                "key": "3RQOUQV1"
            }
        ],
        [
            {
                "start_time": "2024-09-26 03:07:22",
                "end_time": "2024-09-26 12:11:15",
                "splited_start_time": "2024-09-26 03:07:22",
                "splited_end_time": "2024-09-26 12:11:15",
                "key": "NAOO8UZD"
            },
            {
                "start_time": "2024-09-26 04:17:15",
                "end_time": "2024-09-26 13:21:08",
                "splited_start_time": "2024-09-26 04:17:15",
                "splited_end_time": "2024-09-26 13:21:08",
                "key": "RI8IT06L"
            },
            {
                "start_time": "2024-09-26 11:13:35",
                "end_time": "2024-09-26 14:15:12",
                "splited_start_time": "2024-09-26 11:13:35",
                "splited_end_time": "2024-09-26 14:15:12",
                "key": "P17N84F0"
            }
        ]
    ]
}

数据转换思路:

  1. 首先将原始数据进行分割(针对跨天的情况),当元素跨3天,就分割成3个元素
  2. 对分割后的元素基于开始时间进行从早到晚排序
  3. 对排序后的元素进行按天分组
  4. 对按天分组内的元素进行二次分组,将时间段重叠的单独分成一组

4.2 组件文件结构

diff 复制代码
- index.js
- useWeek.js // 控制周切换相关方法
- Controler.js // 控制周切换
- TimeBlock.js // 时间块子组件
- Detail.js // 广播详情展示
- ColorTags.js // 颜色图标提示
- utils.js // 通用方法
- useExpandTime.js
- style.less

是有更优化的结构的,但是实现了就懒得优化了。

4.3 源码部分

index.js 入口文件

jsx 复制代码
import React, { useEffect, useState } from 'react';
import useWeek from './useWeek';
import Controler from './Controler';
import './style.less';
import { formatData, hoursArray } from './utils';
import { Icon } from 'antd';
import { Ajax } from 'src/common';
import ColorTags from './ColorTags';
import useExpandTime from './useExpandTime';
import TimeBlock from './TimeBlock';

const BrCalendarView = () => {
   const { weekInfo, prevWeek, nextWeek, resetToCurrentWeek } = useWeek();
   const { handleExpand, cellProps } = useExpandTime();
   const [activeBlock, setActiveBlock] = useState('');

   const [data, setData] = useState([]);
   const [expandDay, setExpandDay] = useState({
      show: false,
      day: {},
      data: []
   });

   const openModal = (info) => {
      setExpandDay({
         show: true,
         day: info.day,
         data: info.data
      });
   };

   const handleActive = (id) => {
      setActiveBlock(id);
   };

   useEffect(() => {
      /**
       * 发送请求
       *
       * 入参:
       * filter:{
       *     start_time: weekInfo.startDate.datetime,
       *     end_time: weekInfo.endDate.datetime
       * }
       *
       * 重置状态
       * setData(formatData(data.list));
       */
      Ajax.ajax({
         url: 'BrdCalendar',
         showLoading: false,
         data: {
            filter: {
               start_time: weekInfo.startDate.datetime,
               end_time: weekInfo.endDate.datetime
            }
         },
         success: (data) => {
            setData(formatData(data.list));
         }
      });
   }, [weekInfo.startDate.datetime, weekInfo.endDate.datetime]);

   return (
      <React.Fragment>
         <div className="br-calendar-view">
            <Controler prevWeek={prevWeek} weekInfo={weekInfo} resetToCurrentWeek={resetToCurrentWeek} nextWeek={nextWeek} />
            <div className="br-calendar-view__content">
               <ColorTags />
               {/* 表格部分 */}
               <div className="view-table">
                  {/* 头部 */}
                  <div className="view-table-header">
                     <div className="expand relative fr" style={{ width: '138px' }} onClick={handleExpand}>
                        <span style={{ marginRight: '8px' }}>时刻表(展开)</span>
                     </div>

                     {/* 根据天的展开与否显示不同组件 */}
                     {expandDay.show ? (
                        <div
                           className="fc relative expand"
                           style={{ flex: 1 }}
                           onClick={() => {
                              setExpandDay({
                                 ...expandDay,
                                 show: false
                              });
                           }}
                        >
                           <div> {expandDay.day.day}</div>
                           <div>({expandDay.day.shortFormat})</div>
                           <Icon type="fullscreen-exit" className="right" title="返回" />
                        </div>
                     ) : (
                        weekInfo.days.map((item) => {
                           const isExpand = data[item.date] && Math.max(...data[item.date].map((item) => item.length)) > 3;
                           return (
                              <div
                                 className={`fc relative ${isExpand ? 'expand' : ''}`}
                                 onClick={() => {
                                    if (!isExpand) {
                                       return;
                                    }
                                    openModal({
                                       day: item,
                                       data: data[item.date]
                                    });
                                 }}
                              >
                                 <div> {item.day}</div>
                                 <div>({item.shortFormat})</div>
                                 {isExpand && <Icon type="fullscreen" className="right" title="更多" />}
                              </div>
                           );
                        })
                     )}
                  </div>
                  {/* 下方表格 */}
                  <div className="view-table-column">
                     {/* 时间段 */}
                     <div className="column" style={{ width: '138px' }}>
                        {hoursArray.map((item, index) => (
                           <div
                              className="cell"
                              style={{
                                 ...cellProps,
                                 borderRight: '1px solid #eee',
                                 // borderLeft: '1px solid #28568c',
                                 ...(index === 11
                                    ? {
                                         borderBottomColor: 'rgba(104, 185, 255, 0.8)',
                                         borderBottomStyle: 'solid'
                                      }
                                    : {})
                              }}
                              key={item.start}
                           >
                              {item.start}-{item.end}
                           </div>
                        ))}
                     </div>
                     {/* 时间块 */}
                     {expandDay.show ? (
                        <div className="relative" style={{ flex: 1, height: '100%' }}>
                           {hoursArray.map((item) => (
                              <div className="cell" style={cellProps}></div>
                           ))}
                           {expandDay.data.map((blocks) => {
                              let width = 100;
                              return blocks.map((item, index) => (
                                 <TimeBlock
                                    data={item}
                                    width={width}
                                    index={index}
                                    key={item.uuid}
                                    onMouseChange={handleActive}
                                    isAvtive={activeBlock === item.ebmid}
                                 />
                              ));
                           })}
                        </div>
                     ) : (
                        weekInfo.days.map((item) => (
                           <div className="column relative">
                              {hoursArray.map((item, index) => (
                                 <div
                                    className="cell"
                                    key={item.start}
                                    style={{
                                       ...cellProps,
                                       ...(index === 11
                                          ? { borderBottomColor: 'rgba(104, 185, 255, 0.8)', borderBottomStyle: 'solid' }
                                          : {})
                                    }}
                                 ></div>
                              ))}
                              {data[item.date] &&
                                 data[item.date].map((blocks) => {
                                    const length = blocks.length;
                                    let width = Math.floor(100 / Math.min(length, 3));
                                    return blocks
                                       .slice(0, 3)
                                       .map((item, index) => (
                                          <TimeBlock
                                             data={item}
                                             width={width}
                                             index={index}
                                             unit="%"
                                             key={item.uuid}
                                             onMouseChange={handleActive}
                                             isAvtive={activeBlock === item.ebmid}
                                          />
                                       ));
                                 })}
                           </div>
                        ))
                     )}
                  </div>
               </div>
            </div>
         </div>
      </React.Fragment>
   );
};

export default BrCalendarView;

useWeek.js 控制周切换相关方法

js 复制代码
import { useState, useCallback } from 'react';
import { formatDateTime, formatDate } from './utils';

// 获取本周的周一日期
function getMonday(date) {
   const day = date.getDay();
   const diff = day === 0 ? -6 : 1 - day; // 周一为0,周日为6
   date.setDate(date.getDate() + diff);
   date.setHours(0, 0, 0, 0);
   return new Date(date);
}

// 获取本周的周日日期
function getSunday(date) {
   const day = date.getDay();
   const diff = day === 0 ? 0 : 7 - day; // 周一为0,周日为6
   date.setDate(date.getDate() + diff);
   date.setHours(23, 59, 59, 999);
   return new Date(date);
}

// 获取星期名称
function getDayName(date) {
   const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
   return days[date.getDay()];
}

// useWeek hook
function useWeek() {
   const [startDate, setStartDate] = useState(() => getMonday(new Date()));
   const [endDate, setEndDate] = useState(() => getSunday(new Date()));

   const getWeekInfo = useCallback(() => {
      const today = new Date();
      // 周一到周日
      const days = Array(7)
         .fill()
         .map((_, index) => {
            const day = new Date(startDate);
            day.setDate(startDate.getDate() + index);
            const date = formatDate(day);
            return {
               date,
               day: getDayName(day),
               shortFormat: date.split('-').slice(1).join('-')
            };
         });
      const weekInfo = {
         today: {
            date: formatDate(today),
            day: getDayName(today)
         },
         startDate: {
            date: formatDate(startDate),
            day: getDayName(startDate),
            datetime: formatDateTime(startDate)
         },
         endDate: {
            date: formatDate(endDate),
            day: getDayName(endDate),
            datetime: formatDateTime(endDate)
         },
         days,
         isCurrentWeek: days.map((item) => item.date).includes(formatDate(today))
      };
      return weekInfo;
   }, [startDate, endDate]);

   const prevWeek = useCallback(() => {
      const newStartDate = new Date(startDate);
      newStartDate.setDate(newStartDate.getDate() - 7);
      setStartDate(getMonday(newStartDate));
      setEndDate(getSunday(newStartDate));
   }, [startDate]);

   const nextWeek = useCallback(() => {
      const newStartDate = new Date(startDate);
      newStartDate.setDate(newStartDate.getDate() + 7);
      setStartDate(getMonday(newStartDate));
      setEndDate(getSunday(newStartDate));
   }, [startDate]);

   const resetToCurrentWeek = useCallback(() => {
      setStartDate(getMonday(new Date()));
      setEndDate(getSunday(new Date()));
   }, []);

   return { weekInfo: getWeekInfo(), prevWeek, nextWeek, resetToCurrentWeek };
}

export default useWeek;

Controler.js 周切换组件

js 复制代码
import React from 'react';
import { Button } from 'antd';

const Controler = ({ prevWeek, weekInfo, resetToCurrentWeek, nextWeek }) => {
   return (
      <div className="br-calendar-view__header">
         <Button onClick={prevWeek} type="primary">
            上一周
         </Button>
         <div className="current-week-wrapper fc">
            <div className={`week-info ${weekInfo.isCurrentWeek ? 'active' : ''}`}>
               {weekInfo.startDate.date} ~{weekInfo.endDate.date}
            </div>
            {!weekInfo.isCurrentWeek && (
               <a href="javascript:void 0" onClick={resetToCurrentWeek} style={{ fontSize: '1.2em' }}>
                  回到本周
               </a>
            )}
         </div>
         <Button onClick={nextWeek} type="primary">
            下一周
         </Button>
      </div>
   );
};

export default Controler;

TimeBlock.js 时间块组件

jsx 复制代码
import { Tooltip } from 'antd';
import Detail from './Detail';
import { getBlockProps, colorTags } from './utils';
import React from 'react';

const TimeBlock = ({ data, width, index = 0, unit = 'px', onMouseChange, isAvtive = false }) => (
   <Tooltip placement="rightTop" title={<Detail data={data} />} overlayClassName="expandwidth">
      <div
         onMouseEnter={(e) => {
            e.stopPropagation();
            if (onMouseChange) {
               onMouseChange(data.ebmid);
            }
         }}
         onMouseLeave={(e) => {
            e.stopPropagation();
            if (onMouseChange) {
               onMouseChange('');
            }
         }}
         style={{
            width: `${width - 2}${unit}`,
            left: `${width * index + 1}${unit}`,
            ...getBlockProps(data.splited_start_time, data.splited_end_time),
            background: isAvtive ? `rgb(255, 210, 95,1)` : colorTags[data.status].color
         }}
         className="block"
      >
         {data.name}
      </div>
   </Tooltip>
);

export default TimeBlock;

Detail.js 详情组件

js 复制代码
import { Row, Col } from 'antd';
import { colorTags } from './utils';
import { Opts } from 'src/common';
import React from 'react';

const Detail = ({ data }) => {
   const column = [
      {
         label: '广播名称',
         dataKey: 'name'
      },
      {
         label: 'Ebmid',
         dataKey: 'ebmid'
      },
      {
         label: '广播类型',
         dataKey: 'msg_type',
         render: (v) => Opts.getTxt(Opts.g_superiorEbmClass, v)
      },
      {
         label: '开始时间',
         dataKey: 'start_time',
         render: (v) => {
            const [date, time] = v.split(' ');
            return (
               <span>
                  <span>{date}</span>
                  <span style={{ marginLeft: '4px', color: 'rgb(255, 210, 95,1)' }}>{time}</span>
               </span>
            );
         }
      },
      {
         label: '结束时间',
         dataKey: 'end_time',
         render: (v) => {
            const [date, time] = v.split(' ');
            return (
               <span>
                  <span>{date}</span>
                  <span style={{ marginLeft: '4px', color: 'rgb(255, 210, 95,1)' }}>{time}</span>
               </span>
            );
         }
      },
      {
         label: '播发状态',
         dataKey: 'status',
         render: (v) => <span style={{ color: colorTags[v].color }}>{colorTags[v].label}</span>
      },
      {
         label: '覆盖区域',
         dataKey: 'covered_regions'
      },
      {
         label: '创建人',
         dataKey: 'creator'
      }
   ];

   return (
      <div style={{ width: '100%' }}>
         {column.map((item) => (
            <Row>
               <Col span={6}>{item.label}:</Col>
               <Col span={18}>{item.render ? item.render(data[item.dataKey], data) : data[item.dataKey]}</Col>
            </Row>
         ))}
      </div>
   );
};

export default Detail;

ColorTags.js 图例

jsx 复制代码
import { colorTags } from './utils';
import React from 'react';
const ColorTags = () => {
   return (
      <div className="color-tags">
         {Object.values(colorTags).map((item) => (
            <div>
               <div style={{ width: '28px', height: '16px', background: item.color, marginRight: '4px' }}></div>
               <div>{item.label}</div>
            </div>
         ))}
      </div>
   );
};

export default ColorTags;

useExpandTime.js 控制时刻表的展开

js 复制代码
import { cellHeight } from './utils';
import { useState } from 'react';

const type = ['mini', 'medium', 'large'];
const useExpandTime = () => {
   const [expand, setExpand] = useState(0);
   const handleExpand = () => {
      if (expand === 2) {
         setExpand(0);
      } else {
         setExpand(expand + 1);
      }
   };

   return {
      expand,
      handleExpand,
      cellProps: cellHeight[type[expand]]
   };
};

export default useExpandTime;

utils.js 其他方法

js 复制代码
import { Util } from 'src/common';
export function formatDateTime(date) {
   const year = date.getFullYear();
   const month = (date.getMonth() + 1).toString().padStart(2, '0');
   const day = date.getDate().toString().padStart(2, '0');
   const hours = date.getHours().toString().padStart(2, '0');
   const minutes = date.getMinutes().toString().padStart(2, '0');
   const seconds = date.getSeconds().toString().padStart(2, '0');
   return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

// 格式化日期
export function formatDate(date) {
   const year = date.getFullYear();
   const month = (date.getMonth() + 1).toString().padStart(2, '0');
   const day = date.getDate().toString().padStart(2, '0');
   return `${year}-${month}-${day}`;
}

// 常量:一天的秒数
export const MinutesForDay = 1440;

// 计算 时间块顶部到零点相隔的时间 以及 时间块所占用的时间 ,单位分钟
function calculateTimeDifferences(startTime, endTime) {
   // 将时间字符串转换为Date对象
   const start = new Date(startTime);
   const end = new Date(endTime);

   // 创建当天0点的Date对象
   const midnight = new Date(startTime);
   midnight.setHours(0, 0, 0, 0);

   // 计算从0点到start_time的间隔(分钟)
   const diffFromMidnightToStart = (start - midnight) / (1000 * 60);

   // 计算从start_time到end_time的间隔(分钟)
   const diffFromStartToEnd = (end - start) / (1000 * 60);

   // 将结果四舍五入并转换为整数
   const minutesFromMidnightToStart = Math.round(diffFromMidnightToStart);
   const minutesFromStartToEnd = Math.round(diffFromStartToEnd);

   return {
      fromMidnightToStart: minutesFromMidnightToStart,
      fromStartToEnd: minutesFromStartToEnd
   };
}

// 根据起止时间拿到 位置 和 高度 属性
export const getBlockProps = (startTime, endTime) => {
   const { fromMidnightToStart, fromStartToEnd } = calculateTimeDifferences(startTime, endTime);
   const top = ((fromMidnightToStart / MinutesForDay) * 100).toFixed(2);
   const height = ((fromStartToEnd / MinutesForDay) * 100).toFixed(2);
   return {
      top: `${top}%`,
      height: `${height}%`
   };
};

// 处理重叠:对存在时间段重叠的广播进行分组
export function groupOverlapping(items, startkey, endkey) {
   // 初始化分组结果
   const groups = [];
   let currentGroup = [];

   for (let item of items) {
      // 如果当前组为空,则有重叠
      if (currentGroup.length === 0) {
         currentGroup.push(item);
         continue;
      }
      // 当前时间段的开始时间小于数组内最晚的结束时间,则有重叠
      const lastTime = Math.max(...currentGroup.map((item) => new Date(item[endkey]).getTime()));
      if (new Date(item[startkey]).getTime() < lastTime) {
         currentGroup.push(item);
         continue;
      }
      // 否则,当前时间段与当前组没有重叠,开始新的组
      groups.push(currentGroup);
      currentGroup = [item];
   }

   // 将最后一组添加到结果中
   if (currentGroup.length > 0) {
      groups.push(currentGroup);
   }

   return groups;
}

function splitInterval(interval) {
   const intervals = [];
   let currentStart = new Date(interval.start_time);
   let currentEnd = new Date(interval.end_time);

   // 循环直到当前开始时间超过结束时间
   while (currentStart < currentEnd) {
      let endOfDay = new Date(currentStart);
      endOfDay.setHours(23, 59, 59, 999);

      // 如果结束时间早于当天的23:59:59,则使用结束时间
      if (endOfDay > currentEnd) {
         endOfDay = new Date(currentEnd);
      }

      intervals.push({
         ...interval,
         splited_start_time: formatDateTime(currentStart),
         splited_end_time: formatDateTime(endOfDay),
         key: Util.getUUID()
      });

      // 如果当前时间段的结束时间等于原始结束时间,结束循环
      if (endOfDay.getTime() === currentEnd.getTime()) {
         break;
      }

      // 设置下一个时间段的开始时间
      currentStart = new Date(endOfDay);
      currentStart.setHours(0, 0, 0, 0);
      currentStart.setDate(currentStart.getDate() + 1);
   }
   return intervals;
}

// 处理跨天
export function splitIntervals(inputIntervals) {
   const allIntervals = [];
   inputIntervals.forEach((interval) => {
      allIntervals.push(...splitInterval(interval));
   });
   return allIntervals;
}

// 根据日期分组
const groupByDay = (intervals, comparekey = 'start_time') => {
   const groups = {};

   intervals.forEach((interval) => {
      // 获取开始日期的年月日作为键
      const startKey = interval[comparekey].split(' ')[0];

      // 如果该日期还没有分组,则创建一个新组
      if (!groups[startKey]) {
         groups[startKey] = [];
      }

      // 将时间段添加到对应的日期组中
      groups[startKey].push(interval);
   });

   // 将分组对象转换为数组
   return groups;
};

// 格式化原始数据
export const formatData = (data) => {
   // 1. 分割
   const allSplitedData = splitIntervals(data);
   // 2. 排序
   allSplitedData.sort((a, b) => new Date(a.splited_start_time).getTime() - new Date(b.splited_start_time).getTime());

   // 3. 按天分组
   const groups = groupByDay(allSplitedData, 'splited_start_time');
   // 4. 重组
   Object.keys(groups).forEach((key) => {
      groups[key] = groupOverlapping(groups[key], 'splited_start_time', 'splited_end_time');
   });
   return groups;
};

export const colorTags = {
   3: {
      label: '已播发',
      color: 'rgba(193,193,193, 1)'
   },
   2: {
      label: '正在播发',
      color: '#5ca2fb'
   },
   1: {
      label: '等待播发',
      color: '#5dd560'
   }
};

export const hoursArray = [
   { start: '00:00', end: '01:00' },
   { start: '01:00', end: '02:00' },
   { start: '02:00', end: '03:00' },
   { start: '03:00', end: '04:00' },
   { start: '04:00', end: '05:00' },
   { start: '05:00', end: '06:00' },
   { start: '06:00', end: '07:00' },
   { start: '07:00', end: '08:00' },
   { start: '08:00', end: '09:00' },
   { start: '09:00', end: '10:00' },
   { start: '10:00', end: '11:00' },
   { start: '11:00', end: '12:00' },
   { start: '12:00', end: '13:00' },
   { start: '13:00', end: '14:00' },
   { start: '14:00', end: '15:00' },
   { start: '15:00', end: '16:00' },
   { start: '16:00', end: '17:00' },
   { start: '17:00', end: '18:00' },
   { start: '18:00', end: '19:00' },
   { start: '19:00', end: '20:00' },
   { start: '20:00', end: '21:00' },
   { start: '21:00', end: '22:00' },
   { start: '22:00', end: '23:00' },
   { start: '23:00', end: '24:00' }
];

export const cellHeight = {
   mini: {
      height: '28px',
      lineHeight: '28px'
   },
   medium: {
      height: '64px',
      lineHeight: '64px'
   },
   large: {
      height: '300px',
      lineHeight: '300px'
   }
};

style.less

less 复制代码
.expandwidth .ant-tooltip-inner {
   min-width: 370px;
}

// 通用
.color-tags {
   display: flex;
   justify-content: end;
   margin: 4px 0;
   & > div {
      display: flex;
      align-items: center;
      margin-right: 6px;
   }
}
.fc {
   display: flex;
   flex-direction: column;
   align-items: center;
}
.fr {
   display: flex;
   align-items: center;
   justify-content: center;
}
div {
   box-sizing: border-box;
}
.relative {
   position: relative;
}

.view-table-header {
   display: flex;
   background: #6fa9ec;
   color: white;
   font-weight: bold;
   & > div {
      padding: 4px;
      border-right: 1px solid white;
      cursor: default;
   }
}

.view-table-column {
   display: flex;
   margin-top: 2px;
   max-height: 680px;
   overflow: auto;
   &::-webkit-scrollbar {
      width: 10px; /* 设置横向滚动条的高度 */
      height: 10px;
   }
   /* 滚动条轨道 */
   &::-webkit-scrollbar-track {
      background: #f0f0f0; /* 轨道背景颜色 */
      border-top: 1px solid #ccc; /* 轨道与内容的分隔线 */
   }

   /* 滚动条滑块 */
   &::-webkit-scrollbar-thumb {
      background: #ccc; /* 滑块背景颜色 */
      border-top: 1px solid #ccc; /* 滑块与轨道的分隔线 */
   }
   // 通用单元格样式
   .column {
      border-right: 1px dashed #eee;
      height: 100%;
   }
   .cell {
      text-align: center;
      font-size: 1.2em;
      border-bottom: 1px dashed #eee;
      &:nth-child(2n + 1) {
         background: #f8fcff;
      }
   }
   // 时间块
   .block {
      padding: 2px 4px;
      border-radius: 4px;
      background: #9dc2ec;
      position: absolute;
      color: white;
      cursor: pointer;
      min-height: 24px;
      border: 1px solid white;
      overflow: hidden;
   }
}

.br-calendar-view {
   .br-calendar-view__header {
      display: flex;
      justify-content: space-between;
      .current-week-wrapper {
         .week-info {
            font-size: 1.4em;
            &.active {
               color: dodgerblue;
            }
         }
      }
   }
   .br-calendar-view__content {
      .view-table {
         .view-table-header {
            & > div {
               width: 14.28%;
               &.expand {
                  cursor: pointer;
                  &:hover {
                     background-color: #28568c;
                     font-weight: bolder;
                  }
               }
               i.right {
                  position: absolute;
                  right: 10px;
                  font-size: 2em;
                  top: 11px;
               }
            }
         }
         .view-table-column {
            .column {
               width: 14.28%;
            }
         }
      }
   }
}
相关推荐
乐闻x1 小时前
VSCode 插件开发实战(四):使用 React 实现自定义页面
ide·vscode·react.js
irisMoon061 小时前
react项目框架了解
前端·javascript·react.js
web150850966419 小时前
【React&前端】大屏适配解决方案&从框架结构到实现(超详细)(附代码)
前端·react.js·前端框架
CodeToGym9 小时前
使用 Vite 和 Redux Toolkit 创建 React 项目
前端·javascript·react.js·redux
m0_5485147712 小时前
前端三大主流框架:React、Vue、Angular
前端·vue.js·react.js
冴羽1 天前
Solid.js 最新官方文档翻译(6)—— 组件事件处理程序
前端·javascript·react.js
爱喝奶茶的企鹅1 天前
前端调试技巧:从 Console 到 Chrome DevTools 的进阶指南
react.js
等一场春雨1 天前
react websocket 全局访问和响应
前端·websocket·react.js
王同学JavaNotes1 天前
React 基础:剖析 UI 描述之道
前端·react.js·ui
东离与糖宝1 天前
React 事件机制和原生 DOM 事件流有什么区别
前端·javascript·react.js