网站开发流程进度规划,ftp如何备份网站,300元建站,软件维护有哪些内容本文记录我在开发一个时间段管理组件时遇到的问题和思考过程。这是一个典型的看起来简单#xff0c;做起来细节很多的功能。
警告#xff1a;本文包含大量真实踩坑经历#xff0c;阅读时请做好心理准备背景#xff1a;一个看起来很简单的需求
产品…本文记录我在开发一个时间段管理组件时遇到的问题和思考过程。这是一个典型的看起来简单做起来细节很多的功能。警告本文包含大量真实踩坑经历阅读时请做好心理准备背景一个看起来很简单的需求产品经理微笑“小X啊这里需要一个时间段配置功能。用户可以配置多个时间段要求覆盖全天不能重叠每个时间段还要设置一个限制时长。很简单对吧”我自信满满“没问题”3天后…我抓狂“为什么 TimePicker 不支持 24:00:00为什么时间段合并这么复杂为什么验证逻辑这么长”就这样我开始了一段看起来简单实际上细节爆炸的开发之旅问题124:00:00不存在的第一个坑来得猝不及防。我们的后端API使用秒数格式存储时间0-86400秒86400秒代表 24:00:00。这很合理对吧但是Ant Design 的TimePicker组件不支持 24:00:00是的你没看错它只支持 00:00:00 到 23:59:59。当我第一次尝试这样写的时候// ❌ 这样会报错TimePicker 直接给你甩脸色 TimePicker value{moment(24:00:00, HH:mm:ss)} /控制台友好地提示我Invalid time value。我“”解决方案一个看起来很怪但能用的方案既然组件库不配合那我们就自己来搞定我们采用了一个显示层和存储层分离的方案存储层统一使用 86400 秒表示 24:00:00显示层在界面上显示为 23:59:59// 将秒数转为 Moment 对象用于显示constsecondsToMoment(seconds:number|undefined):Moment|undefined{if(secondsundefined)returnundefined// 86400 (24:00:00) 显示为 23:59:59if(seconds86400){returnmoment().startOf(day).add(23,hours).add(59,minutes).add(59,seconds)}// ... 其他转换逻辑}// 将 Moment 对象转为秒数用于存储constmomentToSeconds(m:Moment|undefined):number|undefined{if(!m)returnundefinedconststartOfDaymoment(m).startOf(day)returnm.diff(startOfDay,seconds)}在用户选择时间后我们还需要做一个特殊处理constnormalizeTimeRange(value:[Moment|null,Moment|null]|null){// ...letend_smomentToSeconds(value[1])// 如果用户选择的是 23:59:59 (86399秒)转换为 86400 (24:00:00)if(end_s86399){end_s86400}return{start_s,end_s}}我的内心OS这个方案虽然不是最优雅的但是…还能怎么办呢在组件库的限制下这已经是最实用的折中方案了。至少保证了数据的一致性用户也看不出什么区别手动狗头。经验教训遇到组件库的限制时不要硬刚要学会曲线救国。有时候看起来不完美的方案反而最实用。问题2如何判断时间段是否覆盖全天这是整个组件最核心的逻辑也是我觉得最有意思的部分。想象一下用户可能选择了[08:00-12:00, 10:00-14:00, 14:00-18:00]这样几个时间段。我们需要 合并重叠或相邻的时间段因为前两个重叠了实际是[08:00-14:00, 14:00-18:00] 计算总覆盖时长✅ 判断是否等于 24 小时86400秒合并时间段算法来了首先我们需要将时间段按开始时间排序然后合并重叠或相邻的。这其实是一个经典的区间合并算法constmergeTimePeriodsBySeconds(periods:number[][]):number[][]{if(periods.length0)return[]constsortedperiods.sort((a,b)a[0]-b[0])// 按开始时间排序constmerged:number[][][]let[currentStart,currentEnd]sorted[0]for(leti1;isorted.length;i){const[start,end]sorted[i]// 如果当前时间段与下一个时间段重叠或相邻合并if(startcurrentEnd){currentEndMath.max(currentEnd,end)}else{// 不重叠保存当前时间段开始新的时间段merged.push([currentStart,currentEnd])currentStartstart currentEndend}}merged.push([currentStart,currentEnd])returnmerged}关键点start currentEnd这个判断条件很关键它不仅能合并重叠的时间段还能合并相邻的比如[00:00-01:00]和[01:00-02:00]会合并成[00:00-02:00]。第一次写这个逻辑时我差点漏掉了相邻的情况导致合并结果不对。果然细节是魔鬼啊 判断是否覆盖全天数学时间到constisCoveringFullDayBySeconds(periods:number[][]):boolean{constvalidPeriodsperiods.filter((p)pp.length2p[0]!undefinedp[1]!undefined)if(!validPeriods.length)returnfalse// 合并重叠或相邻的时间段constmergedIntervalsmergeTimePeriodsBySeconds(validPeriods)// 计算总覆盖时长lettotalCoveredSeconds0mergedIntervals.forEach(([startSeconds,endSeconds]){totalCoveredSeconds(endSeconds-startSeconds)})// 86400秒 24小时但考虑到浮点数精度使用 86399returntotalCoveredSeconds86399}一个小细节为什么用 86399而不是 86400因为浮点数运算可能有精度问题使用更稳妥。虽然这里用的是整数秒但养成好习惯总是没错的 第一次写的时候我用的是 86400结果测试时发现有个边界情况过不了。后来改成 86399才解决。这就是为什么测试很重要…问题3如何检测时间段重叠表单验证需要检查时间段之间是否有重叠。这是必须的不然用户可能选了两个重叠的时间段然后系统就懵了…我最初的想法是写一堆if-else判断各种情况A在B左边、A在B右边、A包含B、B包含A…。但是我后来发现了一个超简单的公式一行代码搞定// 判断两个时间段 [startA, endA] 和 [startB, endB] 是否重叠// 重叠的条件max(startA, startB) min(endA, endB)consthasOverlap((){for(leti0;ivalidTimePeriods.length;i){for(letji1;jvalidTimePeriods.length;j){const[startA,endA]validTimePeriods[i]const[startB,endB]validTimePeriods[j]constoverlapStartMath.max(startA,startB)constoverlapEndMath.min(endA,endB)if(overlapStartoverlapEnd){returntrue// 有重叠}}}returnfalse})()我的反应当我第一次看到这个公式时我的表情是 。这也太简洁了吧理解方法如果你觉得这个公式有点绕多画几个区间图就能理解了。比如[08:00-12:00]和[10:00-14:00]→max(08:00, 10:00) 10:00min(12:00, 14:00) 12:0010:00 12:00→ 重叠 ✅[08:00-12:00]和[14:00-18:00]→max(08:00, 14:00) 14:00min(12:00, 18:00) 12:0014:00 12:00→ 不重叠 ❌画图真的是理解算法的好方法问题4用户体验优化 - 历史记录功能 为了让用户操作更方便其实是为了让产品经理满意 我们添加了一个历史记录功能将用户最近选择的时间段保存到 localStorage并在 TimePicker 的底部显示点击即可快速填充。这个功能虽然看起来不起眼但用户反馈很好有时候最有效的优化就是这些小功能。// 保存到历史记录constsaveTimeToHistory(start:string,end:string):void{consthistorygetTimeHistory()constnewItem{start,end,timestamp:Date.now()}// 去重如果已存在相同的时间段先移除constfilteredHistoryhistory.filter((item)!(item.startstartitem.endend))// 新记录放在最前面只保留最近3条constupdatedHistory[newItem,...filteredHistory]constfinalHistoryupdatedHistory.slice(0,3)localStorage.setItem(time_range_history,JSON.stringify(finalHistory))}在 TimePicker 中显示RangePicker // ... renderExtraFooter{() renderHistoryFooter(dataPath, getFieldValue, setFieldValue)} /实现细节使用localStorage存储简单粗暴只保留最近 3 条记录避免界面太乱自动去重相同的时间段不会重复显示新记录放在最前面符合用户习惯一个小坑最开始我忘记去重了结果用户选择了相同的时间段后历史记录里出现了重复项。后来加了去重逻辑才解决。果然细节决定体验啊问题5智能填充剩余时间 ⚡️用户可能只选择了一部分时间段比如只选了上午和下午漏了晚上如果让他们手动一个个添加那太痛苦了。所以我们需要提供一个一键填充剩余时间的功能让系统自动帮用户补全空白时间段。思路很直接 获取已选择的所有时间段 计算哪些时间段还没有被覆盖这就是刚才的calculateOtherTimePeriodsBySeconds函数出场的时候了➕ 自动添加这些时间段consthandleFillRemainingTime(fields,getFieldValue,add){constcurrentPeriodsgetCurrentTimePeriods(fields,getFieldValue)if(currentPeriods.length0){message.warn(没有已选择的时间段)return}// 计算剩余时间段constblankPeriodscalculateOtherTimePeriodsBySeconds(currentPeriods)if(blankPeriods.length0){message.warn(已覆盖全天无需填充)return}// 自动添加剩余时间段blankPeriods.forEach(([start_s,end_s]){add({start_s,end_s})})}计算剩余时间段的逻辑constcalculateOtherTimePeriodsBySeconds(periods:number[][]):number[][]{// 1. 合并重叠或相邻的时间段constmergedIntervalsmergeTimePeriodsBySeconds(periods)// 2. 计算剩余时间段constotherTimePeriods:number[][][]letpreviousEndSeconds0mergedIntervals.forEach(([startSeconds,endSeconds]){// 如果开始时间不等于上一个结束时间说明中间有空白if(startSeconds!previousEndSeconds){otherTimePeriods.push([previousEndSeconds,startSeconds])}previousEndSecondsendSeconds})// 添加最后一个时间段到24:00:00if(previousEndSeconds86400){otherTimePeriods.push([previousEndSeconds,86400])}returnotherTimePeriods}关键点这个功能的关键是先合并时间段否则可能会计算出错误的剩余时间段。比如用户选择了[00:00-01:00]和[01:00-02:00]如果不合并系统会以为00:00-02:00已经被覆盖了但实际上这两个时间段是相邻的应该合并成一个。我第一次写这个功能时忘记先合并了结果计算出的剩余时间段完全不对。后来加了合并逻辑才解决。这就是为什么测试很重要说第二遍了 总结一个看起来简单的组件 这个组件虽然看起来功能简单但在实现过程中遇到了很多细节问题组件库限制TimePicker 不支持 24:00:00需要在显示层做转换第一个坑时间计算合并时间段、判断覆盖、计算剩余时间每一步都需要仔细考虑边界情况算法时间用户体验历史记录、智能填充这些小功能实际使用中很有价值产品经理满意了表单验证复杂的业务规则需要在 validator 中实现但要注意可读性代码质量我的感悟在开发这类业务组件时最大的挑战不是实现某个功能而是在各种限制和需求之间找到平衡点。有时候看起来不优雅的方案反而是最实用的。这也让我明白了一个道理永远不要小看一个看起来简单的需求。每一个简单的需求背后都可能隐藏着无数的细节和坑 ️