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%;
            }
         }
      }
   }
}
相关推荐
中微子35 分钟前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
中微子1 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
前端_学习之路2 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_2 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码2 小时前
1.
react.js·node.js·angular.js
伍哥的传说2 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
Misha韩4 小时前
React Native 一些API详解
react native·react.js
小李飞飞砖4 小时前
React Native 组件间通信方式详解
javascript·react native·react.js
小李飞飞砖4 小时前
React Native 状态管理方案全面对比
javascript·react native·react.js