两个实用功能的实现,流程简化方案A-归类处理和6位卡号输入功能

683 阅读7分钟

大家好,最近整理功能实现方案,整合了一个专栏《实用功能开发指南》,未来会将所有的实用功能方案相关的文章放到这个专栏里。(不排除某个系列单独一个专栏,之后再做其他规划。)

这个专栏里的功能,大部分是我再实际工作中遇到的比较有趣或者复杂点,有些实现过程还挺曲折的,我会将我最终采用的方案写出来,希望能给大家提供一些思路。

今天主要分享两个功能,流程简化方案和6位卡号输入功能。这两个虽然看着八竿子打不到,但是却有相似之处,就是都有很强的针对性,又有很不错的可变性。换言之,它俩像明灯,指向了一条路,从这条路走一趟,可以从中联想到怎么去找更多的路。

接下来,我详细的讲讲这两个功能。

流程简化方案A-归类处理

有方案A,意味着可能会有方案B、C、D......通常,优秀的实践方案是不唯一的。

某个时期,开发者可能会根据最新学到或看到的设计模式,在某个功能点上,改变以往的实现方式。

无一例外的是,开发者致力于写出更优质的代码。

当我接到下面这个流程的需求的时候,我没有着急像以往那样,通过罗列枚举的方式,实现每一条流程路径。

这次,我根据终点的达成条件和交互方式,进行了归类处理。

领取流程的流程图

这次的功能,概括的说,就是用户输入卡号之后领取对应的权益。

一共⑥个流程线路:

1、如果输入的卡号是错误的,给出错误的Modal提示

2、如果卡号正确且没有核销,但是已经超过使用期限,则给出过期的Modal提示

3、如果卡号正确、没有核销且还在可使用期限内,则给卡片的Modal提示

4、如果卡号正确且已经核销,如果此时还没有使用卡权益生成订单或生成的订单是非已生效和非已取消状态,跳转至领取成功页。

5、如果卡号正确且已经核销,如果已经使用卡权益生成订单且订单已生效,跳转至审核已通过页。

6、如果卡号正确且已经核销,如果已经使用卡权益生成订单且订单已取消,跳转至审核未通过页。

归类处理

这里功能的分界点在于卡号是否已核销,核销前主要判断卡的正确性,核销后判断是否有使用卡权益生成的订单以及订单的状态。

错误的卡状态,交互方式是 Modal 提示。而其他条件的交互是页面跳转。

流程处理

cardStatus: 卡的状态,总共3个值。1-卡号错误,2-卡已过期,3-卡正常、

cardExamineStatus: 卡的核销状态,总共2个值。1-卡未核销,2-卡已核销。

orderStatus: 订单状态,总共N个值。因为订单业务是独立的模块,实际的订单状态值很多,但是我们关注的主要是两个状态值,1-订单已生效,2-订单已取消,这两个状态值之外的都会跳转同一个页。

alternatelyType: 交互类型,总共2个值。router-跳转类型,modal-弹出层类型。

/**
 * 卡号输入之后的流程处理
 */
const getPageDistribution = data => {
  // 交互方式 默认跳转类型
  let alternatelyType = 'router';
  // 卡状态异常
  if (data.cardStatus !== 3) {
    alternatelyType = 'modal';
  }
  // 弹出层类型 打开弹出层
  if (alternatelyType === 'modal') {
    let cardModalType = {
      1: cardErrorModal, // 卡号错误的Modal提示
      2: cardExpirationModal, // 卡已过期的Modal提示
    };
    cardModalType[data.cardStatus]();
  } else {
    let cardPathType = {
      1: '/buy', // 卡未核销
      2: getDrowPathByOrderStatus(data.orderStatus), // 卡已核销
    };
    let path = cardPathType(data.cardExamineStatus);
    history.push(path);
  }
};

了解了变量作用和全部的可能值,上面的代码读起来就很顺了。

卡异常状态下,交互都是弹出层类型。所以我设置了一个不同错误的弹出层枚举。

卡正常状态下,交互都是跳转类型。不同核销状态,最终跳转的页面。我同样设置了一个不同跳转地址的枚举。

根据订单状态获取跳转地址

根据订单状态 orderStatus,总分成③种不同的跳转页面:

  • 已生效订单,跳转审核已通过页;
  • 已取消订单,跳转审核未通过页;
  • 其他状态订单,跳转领取成功页。
/**
 * 根据订单状态获取需要跳转页面的地址
 * @param {number} orderStatus 订单状态 0-未生成订单 1-已生效 2-已取消
 * @return {string} path 跳转的地址
 */
getDrowPathByOrderStatus = orderStatus => {
  const pathObj = {
    1: '/effectuate',
    2: '/cancel',
  };
  const commenPath = '/received';
  const path = pathObj[statusType] || commenPath;
  return path;
};

小结

通过交互归类的思路,可以有效的省去部分判断逻辑。

而我之所以特意加上方案A的后缀,是因为通过流程梳理,还有其他的比较好实现方案,欢迎大家留言讨论。

6位卡号输入

前面是根据卡的不同状态的流程实现,接下来,讲讲卡号输入的交互实现。

卡号输入 UI

UI 的呈现,会影响前端的实现方式。这里 UI 设计成弹出层的方式,每个数字都是一个方框。

开发前

在开发前,我列了一些可能出现的问题。比如,不同手机的光标样式、输入时页面抖动、input无法只输入数字等。

所以我在实现的时候,有针对性的规避前面罗列的问题。

功能设计

功能点

首先我罗列了一下主要的功能点,包含了前面提到的问题的规避:

1、唤起手机数字软键盘,只允许输入数字

2、卡号限制6位

3、可删除,删除后光标自动前移,直到第一位

4、重置光标样式

光标样式是浏览器自带样式,考虑重置生效的兼容性和不同浏览器的兼容性

5、光标自动后移

单一的input输入框设计,防止页面抖动

实现

1、唤起手机数字软键盘,只允许输入数字

一般我们将 input 输入框设置为 type='number',此时便只能输入数字了。

但是这个设置在IOS下是不起作用的,即便我又加了pattern="[0-9]*",也还是能输入e、+、-等符号,这个时候,就只需要上正则了。

正则判断数字类型,如果不满足,则将输入值重置为空。

let value = e.target.value;
let regExp = /^[0-9]+$/;
if (!regExp.test(value)) {
  value = '';
}

2、卡号限制6位,光标自动后移

输入框设置为 type='number,maxLength设置值是不会生效的。(如果我没记错的话,只有type='tel',会生效)

所以光标移到最后,如果不做特殊处理,输入框可以一直输入值。

于是我将最后的值做了截取,只保留前6位。

// 卡号数组
let [carmiList, setCarmiList] = useState([]); 
// 光标所在位置
let [currIndex, setCurrIndex] = useState(1);

// 只允许输入单个数字
if (value > 10) {
  value = value.slice(0, 1);
}

// 收集6位卡号
if (currIndex <= 6) {
  carmiList.push(value); 
}

// 当光标到达第6个方框时,卡号只取前面6位
if (currIndex >= 6) {
  nativeInputRef.current.value = value;
  carmiList = carmiList.slice(0, 6);
  setCarmiList([...carmiList]);
  return;
}

3、删除操作

通过监听删除键,处理删除功能。keyCode 的值为8时,表示点击了删除键。

如果此时光标在第一个方框,则清空全部卡号。

如果光标不在第一位,则将卡号数组移除一位,得到新的卡号值,同时光标减去1,得到新的光标位置。

const isBackSpaceKey = e.keyCode === 8;
if (isBackSpaceKey) {
  if (currIndex <= 1) {
    setCarmiList([]);
    setCurrIndex(1);
  } else {
    carmiList.pop();
    let newValue = carmiList.slice();
    setCarmiList([...newValue]);
    setCurrIndex(currIndex - 1);
  }
}

4、重置光标样式

caret-color: 设置 input 元素中光标的颜色。

::first-line: 用来指定选择器第一行的样式。caret-color 无法改变 IOS 的光标颜色,所以又设置了 ::first-line 选择器的颜色用来兼容 IOS。

input {
  caret-color: goldenrod;
  outline: none; 
  &:focus{
    border-color: goldenrod;
  }
  // 兼容Safari
  &::first-line {
    color: goldenrod;
  }
}

5、光标自动后移

为了防止抖动,所以我只保留了一个 input 输入框,通过光标位置更换 input 输入框展示的位置。

<div className='card'>
  {carmiList.map((item, index) => {
    return (
      <Fragment key={index}>
        {currIndex === index ? (
          <input
            className='card-item'
            ref={nativeInputRef}
            key={index}
            id='carmiEle'
            type='number'
            pattern='[0-9]*'
            autoFocus
            autoComplete='off' // 不保存输入的数字
            maxLength='1'
            onChange={e => handleChange(e, index)}
            onKeyDown={e => handleDelete(e, index, item)}
            inputMode='numeric'
          />
        ) : (
          <div className='card-item'>{item.num}</div>
        )}
      </Fragment>
    );
  })}
</div>

躲不开的兼容性问题

测试过程,很顺利,直到 iPhone 12及以上,页面抖动的效果,「出人不意料之外」的出现了。真是令前端「闻风丧胆战心惊」的移动端兼容性

在移动端兴起以前,PC端的兼容性处理,是最令前端头疼的事情之一。但是PC端的兼容性问题,还是比较好复现的,即便开发自己的电脑没有对应的浏览器,身边人也能找到,再不济,装个虚拟机,也能满足。

但是移动端的兼容性,排查起来,就没那么容易了,手机机型的匮乏,是一个问题。另一个问题则是,移动端的环境不像PC端那么直观。

无论问题有多难,还是要解决它。

曲折的问题解决过程

这个抖动,我最开始觉得是因为页面有滚动条,所以每次输入数字之后,input失焦,又很快再次聚焦,导致页面发生了快速的滚动。所以出现了抖动。

所以开始的时候我就想方设法的不让页面产生滚动。

可能方案1

打开卡号输入弹出层的同时,将页面滚动到顶部。

/**
 * 快速定位到顶部
 */
const goTop = () => {
  document.body.scrollTop = document.documentElement.scrollTop = 0;
};

结果,该抖还是抖。

可能方案2

打开弹出层的时候,将页面设置为禁止滚动。

document.body.style.overflow = 'hidden';

结果,该抖还是抖。

可能方案3

所以说禁止页面滚动的方式无法解决问题,连暂时方案都算不上。最根本的还是解决 input 聚焦和失焦导致页面发生抖动的情况。

如果 input 一直不失焦不就行了?我感觉我可能找到了真正的解决方案。

不失焦就表示它一直是输入状态,也就是它不是被替换了而是发生了挪动,也就是说它挪到了下一个方框上。

input 输入框上设置好可移动属性,X轴的值是动态的。

style={{ transform: `translate(${cardX}px, -32px)` }}

输入操作,每次向右移动一个方框+边距的距离;

删除操作,每次向左移动一个方框+边距的距离;

// X轴移动的距离
const [cardX, setCardX] = useState(0); 
// 每次移动的距离
let [drift, setDrift] = useState(0);

// 输入操作
setCardX(cardX + drift); 

// 删除操作
setCardX(cardX - drift);

每次移动的距离,我是算出来的,因为不同手机下的值不一样。

/**
   * 获取每次移动的位移
   */
  const getDriftVal = () => {
    let contentItem = document.getElementsByClassName('content-item')[0];
    let contentClass = document.getElementsByClassName('card-content')[0];
    let itemOffsetWidth = contentItem.offsetWidth;
    let boxOffsetWidth = contentClass.offsetWidth;
    let widthDVal = boxOffsetWidth - itemOffsetWidth * 6;
    let interval = Filter.accuracyExcept(widthDVal, 5);
    drift = Filter.accuracyAdd(interval, itemOffsetWidth) || 56;
    setDrift(drift);
  };

到这,抖动的问题,终于被解决了。

总结

本文,主要分享了两个实用功能:

1、领取流程,看似复杂,采用归类的思路进行实现,有效的省去部分判断逻辑。

2、6位卡号输入功能,该功能可能出现的兼容性问题,以及解决这些问题的思路和方案。兼容性问题,对于前端而言,是一个「拦路虎」。能找到问题的本质,有时候并不容易,但是每一次的寻找答案的过程,都会有所收获。

本次分享的iOS系统input快速聚焦和失焦导致页面抖动的解决方式,希望能帮到每一个有需要的开发者。共勉。

以上就是本次分享的内容。如果觉得有帮助,欢迎留言讨论、点赞 、收藏,持续产出技术分享。


我是 叶一一,非职业「传道授业解惑」的技术博主。「趣学前端」、「CSS畅想」系列作者。

华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。

欢迎技术或非技术问题的讨论。

本文正在参加「金石计划」