React周视图组件封装

news/2024/9/29 16:46:50

技术栈:React、antd

需求背景

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

组件特点

  1. 当多个广播时间段交叠时,并行显示。对于交叠广播,最多显示3个,如果要显示全部交叠的广播,可点击展开。
  2. 可对时间段精度进行扩展。当多个时间短但不重叠的广播放在一起时,更方便看。
  3. 支持点击回到本周。

效果展示

实现

数据结构

本示例的返回数据如下:

{"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":	"省平台"}]}
}

组件文件结构

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

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

源码部分

index.js

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 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));*/}, [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 ? (<divclassName="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 (<divclassName={`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) => (<divclassName="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) => (<TimeBlockdata={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) => (<divclassName="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) => (<TimeBlockdata={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

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,周日为6date.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,周日为6date.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

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

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"><divonMouseEnter={(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

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

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

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

import { Util } from 'src/common'; // 主要使用了一个生成uuid的方法,可以自行封装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 = 'start_time', endkey = 'end_time') {// items.sort((a, b) => a[startkey] - b[startkey]);// 初始化分组结果const groups = [];let currentGroup = [];for (let item of items) {// 如果当前组为空,或者当前时间段的开始时间小于等于当前组最后一个时间段的结束时间,则有重叠if (currentGroup.length === 0 || currentGroup.map((item) => item[endkey]).some((end) => item[startkey] < end)) {currentGroup.push(item);} else {// 否则,当前时间段与当前组没有重叠,开始新的组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) => a.splited_start_time - b.splited_start_time);// 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

.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%;}}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/66221.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

【问题解决】win10日志错误:创建 TLS 客户端凭据时发生致命错误。 内部错误状态为 10013

背景 最近win10死机了一次,查看事件管理器发现有大量的报错:“创建 TLS 客户端凭据时发生致命错误。 内部错误状态为 10013”,如图:解决 win键搜索internet选项原因 参考错误:“ 创建 TLS 客户端凭据时发生致命错误。 内部错误状态为 10013”的说法是win10对TLSv3.0兼容性…

2024-2025-1 20241415 《计算机基础与程序设计》第1周学习总结

这个作业属于哪个课程 2024-2025-1-计算机基础与程序设计(https://edu.cnblogs.com/campus/besti/2024-2025-1-CFAP))这个作业要求在哪里 2024-2025-1计算机基础与程序设计第一周作业这个作业的目标 阅读浏览教材《计算机科学概论》,加深对计算机科学的理解,提高自学能力,学…

Prism 行为处理

Prism框架提供了DelegateCommand类型,专门用于进行WPF中的行为处理。 基本使用一、命令的使用DelegateCommand(Action executeMethod):DelegateCommand的构造函数,创建DelegateCommand对象。 executeMethod:无参的命令执行函数。定义命令public class MainViewModel {publi…

南沙C++信奥赛老师解一本通题1217:棋盘问题

​【题目描述】在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放 kk 个棋子的所有可行的摆放方案 CC。【输入】输入含有多组测试数据。 每组数据…

文件传输 --- 使用 FTP 在两个主机之前传输文件

FTP 客户端 服务端tcpsvd -vE 0.0.0.0 21 ftpd /app/updater/ -w &共享 /app/updater 的文件给客户端

高可用集群 KEEPALIVED ubuntu使用

1 Keepalived 架构和安装 2.1 Keepalived 架构 Keepalived进程树Keepalived <-- Parent process monitoring children \_ Keepalived <-- VRRP child \_ Keepalived <-- Healthchecking child2.2 Keepalived 环境准备 #环境准备 #两台keepalive机器分别配一个单独网卡…

PHP支付,TP5.0接入支付宝支付流程

一、支付宝沙箱 1.登录支付宝开放平台https://open.alipay.com/;点击右上角的“控制台”菜单 2.下拉到页末找到“沙盒” 配置一下基础信息:配置一下信息,特别注意,网关地址:沙箱环境是有dev的,正式上要去掉 dev; 二、DEMO 1.下载电脑网站支付Demo php版本 2.下载后把整…

无法访问你试图使用的功能所在的网络位置

无法访问你试图使用的功能所在的网络位置、无法删除 xxxx工具的旧版本问题如标题,被这个问题搞吐了。报错如下如:     起因是公司的产品有些周边工具,在分析和排查问题的过程中,遇到上图这个问题,因为要反复卸载和重装,还涉及到不同版本,最后玩坏了。卸载以后,Wind…