细说如何封装一个日历组件(多视图、可选择、国际化)

news2024/11/25 13:17:33

前言

最近好奇日历组件是怎么实现的。于是阅读了下react-calendar的源码,并实现了简化版的日历组件。本文把实现日历的设计思路分享给大家。只要理清了主要逻辑,就不难实现了。

技术栈:react、typescript

预览

在线预览demo:coder-xuwentao.github.io/react-mini-…

主要功能

  • 可选择日期
  • 可选择日期范围
  • 支持十年视图、年视图、月视图
  • 国际化
  • 支持最大/最小不可选

Api

export type CalendarProps = {
  defaultValue?: Value; // 默认选择的日期值
  value?: Value; // 选择的日期值
  showNavigation?: boolean; // 是否展示导航栏
  locale?: string; // 地区
  selectRangeEnable?: boolean; // 是否支持选取范围
  className?: string;
  onChange?: (value: Value) => void; // 点击日历导致value变化时的事件勾子
  onClickDay?: OnChangeFunc; // 点击日
  onClickMonth?: OnChangeFunc; // 点击月
  onClickYear?: OnChangeFunc; // 点击年
  
  // StartDate,指的是当前日历组件展示的开头日期。一般在view改变时更新StartDate
  onActiveStartDateChange?: (args: onActiveStartDateChangeArgs) => void;
  calendarRef?: React.Ref<HTMLDivElement>;  // 日历组件的ref
  defaultView?: View; // 默认的视图:十年、年、月
  maxDate?: Date; // 最大
  minDate?: Date; // 最小
  [key: string]: any;
};
// value为Date时,选择的是一个日期
// value为[Date, Date]时,选择的是日期的范围
export type Value = Date | [Date, Date] | undefined;
// 视图
export enum View {
  'Decade',
  'Year',
  'Month',
}

使用例子

const locale = 'zh-CN';
function CalendarDemo() {
  const [value, onChange] = useState<Value>(new Date());

  return (
    <Calendar value={value} onChange={onChange} locale={locale} selectRangeEnable />
  );
}

设计思路:

首先日历分为上下两个部分导航栏和视图。

每个视图都有遍历可点击的单元格按钮,比如下方的“X日”。(下文就都叫单元格按钮 或 单元格)

视图

日历有三个维度的视图,从大到小为:十年(Decade)、年(year)、月(Month)。

视图范围

视图都需要一个范围,比如上图年视图,从 2023-1 到 2023-12;月视图,从2023-6-1到2023-6-30。

我们只需要用一个状态来记录范围的起始日期即可(activeStartDate)。各个视图会根据activeStartDate,在遍历渲染单元格按钮时,把日期值关联到按钮,方便展示和获取日期。

视图之间的切换

两种方式:

向下深入(DrillDown) 方式:点击单元格按钮,视图从高维变低维。

处理逻辑:

  1. 更新范围值activeStartDate
  2. 切换视图view

向上弹出(DrillUp) 方式:点击导航栏的中间按钮。 处理逻辑:

  1. 判断当前是否可以继续弹出
  2. 切换视图view

activeStartDate不需更新,因为此时的activeStartDate显然在新view的范围内。

部分代码预览:

  // Calendar.tsx
  // 深入到月
  const haddleDrillDownToMonth = useCallback((monthIdx: number, event: React.MouseEvent) => {
    setViewState(View.Month); // 切换视图view
    const nextStartDate = getDateBySetMonth(activeStartDateState, monthIdx);
    setActiveStartDate(nextStartDate, 'drillDown'); // 更新范围值activeStartDate
    onClickMonth?.(nextStartDate, event);
  }, [activeStartDateState, setActiveStartDate, onClickMonth]);

  // 深入到年
  const haddleDrillDownToYear = useCallback((year: number, event: React.MouseEvent) => {
    setViewState(View.Year); // 切换视图view
    const nextStartDate = getDateBySetYear(activeStartDateState, year);
    setActiveStartDate(nextStartDate, 'drillDown'); // 更新范围值activeStartDate
    onClickYear?.(nextStartDate, event);
  }, [activeStartDateState, setActiveStartDate, onClickYear]);

  // 在月视图点击“日”单元格按钮,显然无需做深入操作
  const handleClickDay = () => {}

  // 向上弹出
  const handleDrillUp = useCallback(() => {
    const drillUpAvailable = sortedViews.indexOf(viewState) > 0;
    // 判断当前是否可以继续弹出
    if (drillUpAvailable) {
      // 切换视图view
      // 其中sortedViews为 [View.Decade, View.Year, View.Month];
      setViewState(sortedViews[sortedViews.indexOf(viewState) - 1]);
    }

  }, [viewState]);

导航栏

功能

  1. 点击中间标签,则向上切换视图view。
  2. 点击两侧按钮,会改变当前视图范围 - 即改变activeStartDate
    • 如上图,当前展示为2023年6月,点击“‹”会把范围调整到上一个月(2023-5),点击“«”会把范围调整到上一年(2022-6)

注意点

导航栏的中间标签的展示、以及两侧四个按钮的onClick逻辑,在不同视图view有不同的逻辑。比如在年视图时,点击“‹”会将当前日期减1年,而在月视图时,点击“‹”会将当前日期减1月。

可以用switch...case来分开各个view的逻辑。

部分代码预览

 

typescript

复制代码

// 中间标签展示的内容 const defaultLabel = (() => { switch (view) { case View.Decade: // 十年视图 // getDecadeFromDate获取十年的范围 return formatDecade(getDecadeFromDate(date), locale); case View.Year: // 年视图 return formatYear(date, locale); case View.Month: // 月视图 return formatMonthYear(date, locale); default: throw new Error(`Invalid view: ${view}.`); } })(); // 其中 formatXXX 函数的作用是格式化date。详见下面“格式化”

 

typescript

复制代码

// 点击"‹"时,计算最新的activeStartDate // ...'»'、'›'等逻辑类似, 这里就不列举了 export function getDatePrevious(view: View, date: Date): Date { const newDate = new Date(date); switch (view) { case View.Decade: // 十年视图 return getForeYear(newDate, -10); // 当前日期减10年 case View.Year: // 年视图 return getForeYear(newDate, -1); // 当前日期减1年 case View.Month: // 月视图 return getForeMonth(newDate, -1); // 当前日期减1月 default: throw new Error(`Invalid view type: ${view}`); } } // 当前日期的月份加num export function getForeMonth(date: Date, num: number) { const newDate = new Date(date); newDate.setMonth(newDate.getMonth() + num); return newDate; } // 当前日期的年份加num export function getForeYear(date: Date, num: number) { const newDate = new Date(date); newDate.setFullYear(newDate.getFullYear() + num); return newDate; }

格式化(国际化)

组件展示日期时,使用了ECMAScript 的国际化 API - Intl.DateTimeFormat.prototype.format()来进行格式化日期。

为了避免重复new对象造成消耗,在getFormatter做了两层缓存处理:第一层的key是locale,第二层的key是format()的选项options。

相关代码:

 

typescript

复制代码

type Options = Intl.DateTimeFormatOptions; const localeToFormatterCache = new Map(); // key是locale, value是formatterCache function getFormatter(options: Options) { return function formatter(date: Date, locale = 'en-US') { if (!localeToFormatterCache.has(locale)) { localeToFormatterCache.set(locale, new Map()); } // key是 options, value是Intl的format方法 const formatterCache = localeToFormatterCache.get(locale); if (!formatterCache.has(options)) { formatterCache.set( options, new Intl.DateTimeFormat(locale, options).format, ); } return formatterCache.get(options)(date); }; } const formatDayOptions: Options = { day: 'numeric' }; const formatMonthOptions: Options = { month: 'long' }; const formatMonthYearOptions: Options = { month: 'long', year: 'numeric', }; const formatShortWeekdayOptions: Options = { weekday: 'short' }; const formatYearOptions: Options = { year: 'numeric' }; const formatTimeOptions: Options = { day: 'numeric', month: 'long', year: 'numeric', hour: "2-digit", minute: "2-digit", second: "2-digit", }; export const formatDay = getFormatter(formatDayOptions); export const formatMonth = getFormatter(formatMonthOptions); export const formatMonthYear = getFormatter(formatMonthYearOptions); export const formatShortWeekday = getFormatter(formatShortWeekdayOptions); export const formatYear = getFormatter(formatYearOptions); export const formatTime = getFormatter(formatTimeOptions); export function formatDecade ([start, end]: [Date, Date], locale?: string) { return `${formatYear(start, locale)} - ${formatYear(end, locale)}` } // 使用方式:formatMonthYear(date, locale);

细说月视图

这里挑出比较难的月视图来讲下。理解了月视图,也就理解了另外两个视图。

主要功能实现逻辑

  • 从activeStartDate找出日历范围(start、end),然后再根据start、end遍历渲染单元格。

    • 根据start、end,计算出日期信息,并将其绑定到日单元格,方便点击时获取对应日期。
  • 判断单元格日期:

    • 是否被选中(见下图1,图2)。
    • 是否在hover范围内(见下图3)。
    • 是否禁止点击(见下图4)
    • 是否在周末
    • 是否在相邻月份

以下是的四张图,可以辅助理解代码,表现分别为:

  1. 点击第一个日单元格
  2. 选择一个日单元格后,hover某个日单元格
  3. 点击第二个日单元格
  4. 当前月份的1、2日,在最小值minDate之外

相关代码

注释中有详细解释原理。也可以直接看源码,代码里的变量命名尽量做到了名副其实。

 

typescript

复制代码

// MonthView/Days.tsx import Day from './Day'; // ...其他import const className = 'mini-calendar__month-view__days'; export default function Days(props: DaysProps) { const { activeStartDate, locale, onClickDay, value, selectRangeEnable, maxDate, minDate, } = props; // 鼠标hover到的日单元格按钮对应的日期 const [hoverDate, setHoverDate] = useState<Date | null>(null); // 从activeStartDate获取所处年、月 const startYear = useMemo(() => activeStartDate.getFullYear(), [activeStartDate]); const startMonth = useMemo(() => activeStartDate.getMonth(), [activeStartDate]); // activeStartDate处于周几。其中,周日时getDay()为0, 这里改为7. const dayOfWeek = activeStartDate.getDay() || 7; // activeStartDate所处月份天数 const daysInMonth = getDaysInMonth(activeStartDate); // start和end,标记当前月份下的日期范围。 // 为了展示完整的一周,开头和结尾会考虑临近的省份, // 所以start有概率是负数, end有概率大于当前月份天数。 const start = -dayOfWeek + 2; // “-dayOfWeek+2”理解: // 首先dayOfWeek指的是在当月第一天处于周几。 // 2023.6.1是周四,start = -dayOfWeek + 2 = -4 + 2 = -2。 // 于是我们就知道,start为当月第-2天,也就还需要展示前一月的三天(-2=>-1=>0)。 // 。。。 // 而负数也可以用作new Date中day入参,然后计算出前一月的日期 // 比如开头的日期为:new Date(2023, 5/* 6月 */, -2/* start */) // 为2023-5-29 const end = (() => { // 当月的最后一天。 const activeEndDate = new Date(startYear, startMonth, daysInMonth); // 当月的最后一天 距离本周结束还剩几天 // 比如2023.6.30是周五。7 - 5 = 2,还剩两天 // 加上daysInMonth,则end为 32 // 。。。 // 而new Date的day入参超出当月天数范围,。后计算出后一月的日期 // 所以结尾日期为:new Date(2023, 5/* 6月 */, 32/* end */) // 即2023-7-2 const daysUntilEndOfTheWeek = 7 - activeEndDate.getDay(); if (daysUntilEndOfTheWeek === 7) { // getDay()为0,即此时刚好是周日,直接返回daysInMonth return daysInMonth; } return daysInMonth + daysUntilEndOfTheWeek; })(); // 事件:点击日单元格 const handleClickDay = useCallback((event: React.MouseEvent) => { if (!(event.target instanceof HTMLButtonElement)) { return; } // 用了事件委托, 会直接把日期记录每个单元格按钮的dataset上 const { date: dateStr } = event.target.dataset; const clickedDate = new Date(dateStr!); // 将相关日期回调到父组件calendar上处理,比如更新value值 onClickDay?.(new Date(clickedDate), event); }, [onClickDay]); // 事件:hover在日单元格上时 // 虽然是监听onMouseMove事件,但不需要debounce, 否则会卡。 const handleHoverIn = useCallback((event: React.MouseEvent) => { // 如果没开启 选择日期范围 的功能,那么就不处理此事件了。 if (!selectRangeEnable) { return; } if (!(event.target instanceof HTMLButtonElement)) { return; } const { date: dateStr } = event.target.dataset; const hoveredDate = new Date(dateStr!); setHoverDate(hoveredDate) }, [selectRangeEnable]); const handleHoverOut = useCallback(() => { if (!selectRangeEnable) { return; } setHoverDate(null) }, [selectRangeEnable]); // 此日期是否被选择。(即是否等于value,或者在value数组内) // 效果见上图1和图3 const isActiveDate = useCallback((date: Date) => { // 如果value为数组形式,即此时value值的是一个范围。 if (value instanceof Array) { return isInDatesRange(date, value) || areDatesEqual(date, value && getDayStart(value[0]) ) || areDatesEqual(date, value && getDayStart(value[1]) ) } return areDatesEqual(date, value && new Date(value.getFullYear(), value.getMonth(), value.getDate()) ) }, [value]) // 此日期是否禁止点击。在minDate ~ maxDate才可以点击。 const isDisabledDay = useCallback((date: Date) => { let disabled = false; if (maxDate && !areDatesEqual(date, getDayStart(maxDate))) { disabled ||= date.getTime() > maxDate.getTime() } if (minDate && !areDatesEqual(date, getDayStart(minDate))) { disabled ||= date.getTime() < minDate.getTime() } return disabled; }, [minDate, maxDate]) // 此日期是否在 hover下日期 和 value日期之间 // 效果见上图2 const isHover = useCallback((date: Date) => { if (!hoverDate) { return false; } if (value instanceof Array || value === undefined) { return false; } return isInDatesRange(date, [hoverDate, value].sort((a, b) => a.getTime() - b.getTime())) }, [hoverDate, value]) function renderDays() { const dayTiles = []; for (let dayPoint = start; dayPoint <= end; dayPoint += 1) { // 计算出每个单元格对应的日期,并绑定到日单元格中 const date = new Date(startYear, startMonth, dayPoint); dayTiles.push( <Day key={dayPoint} date={date} dayPoint={dayPoint} locale={locale} disabled={isDisabledDay(date)} isActive={isActiveDate(date)} isHover={isHover(date)} /> ); } return dayTiles; } return ( <div className={className} onClick={handleClickDay} onMouseMove={handleHoverIn} onMouseLeave={handleHoverOut}> {renderDays()} </div> ); }

 

typescript

复制代码

// MonthView/Day.tsx const tileClassName = 'mini-calendar-tile'; // 用于提取day、month、decade等按钮的公共样式 export default function Day({ date, dayPoint, locale, isActive, isHover, disabled }: DayProps) { const year = date.getFullYear(); const month = date.getMonth() + 1; const dayOfMonth = date.getDate(); const dayOfWeek = date.getDay(); const dateStr = `${year}-${month}-${dayOfMonth}`; // 是否是周末 const isWeekEnd = (dayOfWeek % 6 === 0) || (dayOfWeek % 7 === 0); // 此日单元格是否是相邻省份的。 // 比如dayPoint如果小于0,显然是上一个月的。而date.getDate()必不会是负数,所以不相等。 const isNeighboringMonth = dayOfMonth !== dayPoint; return ( <button className={classnames(className, tileClassName, { [`${tileClassName}--active`]: isActive, [`${className}--neighboringMonth`]: isNeighboringMonth, [`${className}--weekend`]: isWeekEnd, [`${className}--hover`]: isHover, })} data-date={dateStr} disabled={disabled} > {formatDay(date, locale)} </button> ); }

选择单项、选择范围

组件prop selectRangeEnable为true时,即开启日期范围选择功能。否则只能选择单项

那么如何区分单个日期选择值、日期范围选择值?

  • 代码中的value记录着选择值。
  • 如果selectRangeEnable prop为false,那么每次点击日期value都是Date类型
  • 如果selectRangeEnable prop为true,那么第一次点击日期是Date类型,第二次不同的日期是两个Date构成的数组,也就形成了日期范围。

相关代码:

  // Calendar.tsx
  type Value = Date | [Date, Date] | undefined;
  
  const [valueState, setValueState] = useState<Value>(defaultValue);
  const setValue = useCallback((newValue: Value) => {
    if (valueProp === undefined) {
      setValueState(newValue);
    }
    
    onChange?.(newValue); // 回调给用户的事件
  }, [valueProp, onChange]);
  
  // 处理<Days />组件中的onClickDay事件
  const handleClickDay = useCallback((date: Date, event: React.MouseEvent) => {
    onClickDay?.(date, event); // 回调给用户的事件
    // selectRangeEnable prop为true
    // 并且第一次已经点过了
    if (selectRangeEnable && value instanceof Date) {
      // 如果第二次点击是同一个日期,则跳过
      if (value.getTime() === date.getTime()) {
        return;
      } else {
        // 排序下作为日期范围的value
        setValue([value, date].sort((a, b) => a.getTime() - b.getTime()) as [Date, Date]);
      }
    } else {
      // selectRangeEnable 为 false,
      // 或者selectRangeEnable为 true,但是是第一次点击日期
      setValue(date);
    }
  }, [value, setValue, onClickDay, selectRangeEnable]);

其他细节

  1. 各个视图的单元格按钮样式类似,于是将公共样式提取了到一个类名中 - mini-calendar-tile 
.mini-calendar-tile {
  &:enabled:hover {
    background: #e6e6e6;
  }
  &:disabled {
    color: rgba(16, 16, 16, 0.3);
  }

  &--active {
    color: white;
    background: #006edc;
  }

  &--active:enabled:hover,
  &--active:enabled:focus {
    color: white;
    background: #1087ff;
  }
}

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

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

相关文章

亚马逊云科技中国峰会:探索强化学习的未来与Amazon DeepRacer赛车比赛

目录 一、如何构建自己的第一个强化学习模型第一步: 创建 AWS DeepRacer 资源第二步: 定义你的赛道第三步: 训练你的模型第四步: 优化你的模型第五步: 在仿真器中测试你的模型第六步: 在真实赛道上测试你的模型 二、Amazon DeepRacer 中国峰会总决赛三、Amazon DeepRacer 自动驾…

Redis基础+使用+八股文!万字详解一篇就够!

一、目标 学习Redis基础必须掌握的内容&#xff1a; 了解 Redis 以及缓存的作用&#xff1b;掌握 Redis 5 大基本数据类型的使用&#xff1b;掌握常见Redis 面试题&#xff1b;掌握 Redis 的持久化功能&#xff1b;了解 Redis 集群功能。 二、什么是缓存&#xff1f; 缓存定义…

Netty中PileLine类介绍

一、Netty基本介绍 Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具&#xff0c;用以快速开发高性能、高可靠性的网络服务器和客户端程序。Netty 在保证易于开发的同时还保证了其应用的性能&#xff0c;稳定性和伸缩性。 Netty 是一…

VTK Filter 总结

源对象 成像滤波器 可视化滤波器 可视化滤波器&#xff08;输入类型vtkDataSet&#xff09;。 可视化滤波器&#xff08;输入类型vtkPointSet) 可视化滤波器&#xff08;输入类型vtkPolyData) 可视化滤波器&#xff08;(输入类型vtkStructuredGrid)。 可视化滤波器&#xff08;…

浅析视频监控技术及AI发展趋势下的智能化视频技术应用

视频监控技术是指通过摄像机对指定区域进行实时视频直播、录制、传输、存储、管理和分析的技术系统。它可以用于监控各种场所&#xff0c;如校园、工厂、工地、工作场所、公共区域、交通工具等。视频监控技术主要涉及到以下几个部分&#xff1a; 1、摄像机 摄像机是视频监控技…

三年软件测试外包的我也没能转正

外包的群体庞大&#xff0c;很多企业为了节约高昂的人力成本&#xff0c;会把一些非核心业务承包给外包公司&#xff0c;这些工作往往是阶段性、辅助性&#xff0c;没有什么技术含量&#xff0c;而且由于外包人员不是与大厂签订劳动合同&#xff0c;因此&#xff0c;他们更像是…

图像点运算之灰度变换之非线性变换

目录 note code test note 图像点运算之灰度变换之非线性变换 例如&#xff1a;y 10 * x ^ 0.5 code void noline_convert_fun(uchar& in, uchar& out) {out 10 * (uchar)pow((float)in, 0.5); } void img_nonline_convert(Mat& src, Mat& res) {if (s…

html好看的登录界面2(十四种风格登录源码)

文章目录 1.登录风格效果说明1.1 凹显风登录界面1.2 大气简洁风登录界面1.3 弹出背景风登录界面1.4 动态左右切换风登陆界面1.5 简洁背景切换登录界面1.6 可关闭登录界面1.7 蒙蒙山雨风登录界面1.8 苹果弹框风登录界面1.9 上中下青春风登录界面1.10 夏日风登录界面1.11 星光熠熠…

【从零开始玩量化20】BigQuant平台策略代码本地化(与Github同步)

引言 最近发现了个不错的量化平台&#xff0c;BigQuant BigQuant的客服找到我&#xff0c;推荐他们平台给我使用&#xff0c;宣传的是人工智能&#xff0c;里面可以使用类似ChatGPT的聊天机器人&#xff0c;和可视化拖拉拽功能实现策略。 不过&#xff0c;这些都是锦上添花的…

单体 V/s 分布式架构

这是软件架构模式博客系列第 2 章,我们将讨论单体 V/s 分布式架构。 在软件领域,存在多种架构风格可供选择,我们需要关注不同架构风格带来的风险。选择符合业务需求的架构风格是一个长期迭代的过程。 架构风格可以分为两大主要类型:单体架构(将所有代码部署在一个单元中…

Rancher:外部服务连接K8S-MongoDB服务

Rancher&#xff1a;外部服务请求K8S-MongoDB服务 一、前置条件二、「Layer 4 」与「Layer 7」Load Balancing的区别三、部署容器化MongoDB四、Load Banlancer of Service五、mongoDB验证连接六、总结 #参考链接 [1] How access MongoDB in Kubernetes from outside the clust…

树莓派4B多串口配置

0. 实验准备以及原理 0.1 实验准备 安装树莓派官方系统的树莓派 4B&#xff0c;有 python 环境&#xff0c;安装了 serial 库 杜邦线若干 屏幕或者可以使用 VNC 进入到树莓派的图形界面 0.2 原理 树莓派 4B 有 UART0&#xff08;PL011&#xff09;、UART1&#xff08;mini …

腾讯安全周斌:用模型对抗,构建新一代业务风控免疫力

6月13日&#xff0c;腾讯安全联合IDC发布“数字安全免疫力”模型框架&#xff0c;主张将守护企业数据和数字业务两大资产作为企业安全建设的核心目标。腾讯安全副总裁周斌出席研讨论坛并发表主题演讲&#xff0c;他表示&#xff0c;在新技术的趋势影响下&#xff0c;黑灰产的攻…

TS系列之any与unknown详解,示例

文章目录 前言一、一个示例二、示例目的1、功能描述2、主要区别3、代码实现 总结 前言 本片文章主要是在写ts时遇到不知道类型&#xff0c;很容易就想到用any可以解决一切&#xff0c;但这样写并不好。所以今天就总结学习一下&#xff0c;比较好的处理任意类型的unknown。 一、…

patroni+etcd+antdb高可用

patronietcdantdb高可用架构图 Patroni组件功能 自动创建并管理主备流复制集群&#xff0c;并且通过api接口往dcs(Distributed Configuration Store&#xff0c;通常指etcd、zookeeper、consul等基于Raft协议的键值存储)读取以及更新键值来维护集群的状态。键值包括集群状态、…

MySQL ibdata1 文件“减肥”记

夏天来了&#xff0c;没想到连 ibdata1 文件也要开始“减肥”了~ 作者&#xff1a;杨彩琳 爱可生华东交付部 DBA&#xff0c;主要负责 MySQL 日常问题处理及 DMP 产品支持。爱好跳舞&#xff0c;追剧。 本文来源&#xff1a;原创投稿 有句话是这么说的&#xff1a;“在 InnoDB…

深入分析 Java IO (一)概述

目录 一、前言 二、基于字节操作的接口 2.1、字节输入流 2.2、字节输出流 三、基于字符操作的接口 3.1、字符输入流 3.2、字符输出流 四、字节与字符的转化 4.1、输入流转化过程 4.2、输出流转化过程 五、基于磁盘操作的接口 六、基于网络操作的接口 6.1、Socket简…

接口自动化测试框架?你真的会封装吗?自动化框架几大功能专项...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 当准备使用一个接…

项目经理如何做好时间管理?

1、建立时间管理原则 &#xff08;1&#xff09;我们需要通过时间日志的方式对时间进行记录和分析&#xff0c;并对日常要处理的事务进行优先级排序&#xff0c;优先处理最重要的事物&#xff1b; &#xff08;2&#xff09;确定待处理事物的机会成本&#xff0c;提高时间使用…

建模助手618 | 谁不囤点Revit插件我都会生气!

大家好&#xff0c;这里是建模助手。 早在5月份&#xff0c;我们已经就“618”这个事情高调了一番&#xff0c;以提前放“价”的姿势&#xff0c;让许多用户以躺赢的状态拉开了年中大促的序幕。&#xff08;5月购买的盆友&#xff0c;切记看完全文&#xff0c;内附彩蛋 活动反…