实现星级评分(半星)组件

900 阅读2分钟

image.png 携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天。点击查看活动详情

需求背景

最近开发公司自己的组件库,涉及到了系统评分系统,心想这不是就和高贵优雅的UI设计师拿两张上色和没上色的底图就完了。当我找到UI小姐姐要图时,她给我的回答是没有图只有图标,放在Iconfont里自己自取,并且要实现半星的效果(UI组件规范基本由我们UI设计同学定制)。接着又说了图标随着迭代随时都会变,是一个自定义的评分组件。

思路

因为要实现半星,而且UI小姐姐不给卑微的我切图,自然是不能插入无色背景图、半色背景图、和满色背景图。

  1. 这里我使用CSS伪类来实现半星,添加未选择、选择一半、全选三种样式。
  2. 通过每个图标offsetWidth,算出当前点击图标的总距离减去半个图标的宽度的长度,再用clientX与之对比判断当前点击图标是在图标的左侧还是右侧进而判断是否是半星还是满星。
  3. 计算得分,每个图标的索引实际就是分数,并给每个图标加上id。若是allowHalf为true,鼠标点击当前图标在左侧加0.5分,右侧则加1分。
  4. 通过id和当前得分分别对应(未选、半选、全选)三种样式。
  5. 到这里,基本大功告成,但是因为公司需要自定义Iconfont图标,这里为了可以随着版本的迭代,随时更新图标,这里用到了CSS3中伪类content的属性attr(data-content)。这样就能自定义你想要的图标啦。😄

上代码

  1. Rate.tsx

import "./index.scss";
import "../assets/fonts/iconfont.css";

interface IProps {
  defaultValue?: number;
  allowHalf?: boolean;
  onChange: (val: number) => void;
}
const Rate = (props: IProps) => {
  const { defaultValue = 0, allowHalf = true, onChange } = props;
  const RateIcon = (iconTag: { iconId: number }) => {
    const { iconId } = iconTag;
    return (
      <>
        <div
          icon-rate-id={iconId}
          data-content={"\ue625"}
          className={[
            "iconfont",
            score === 0
              ? "unSelected"
              : score - iconId === -0.5
              ? "halfSelected"
              : score - iconId >= 0
              ? "selected"
              : "unSelected",
          ].join(" ")}
        >
          <i className="iconfont" style={{ fontSize: 30 }}>
            &#xe625;
          </i>
        </div>
      </>
    );
  };

  useEffect(() => {
    setscore(defaultValue);
  }, []);

  const [score, setscore] = useState(0);
    const countScore = (event: React.MouseEvent, i: number) => {
    let currentScore = 0;
    const el = event.target as HTMLDivElement;
    const elWidth = el.offsetWidth;
    const clientX = event.clientX;
    if (allowHalf) {
      if (clientX > elWidth * (i + 1) - elWidth / 2) {
        currentScore = i + 1;
        if (i + 1 === score) {
          currentScore = 0;
        }
      } else {
        if (score && score % (i + 1) === score && i + .5 <= score) {
          currentScore = 0;
        } else {
          currentScore = i + 0.5;
        }
      }
    } else {
      currentScore = i + 1;
      if (i + 1 === score) {
        currentScore = 0;
      }
    }

    setscore(currentScore);
    onChange(currentScore);
  }

  return (
    <div className="selectContainer">
      {Array.from({ length: 5 }, (item, i) => (
        <div
          key={`fe-rate-${i + 1}`}
          style={{ display: "inline-block" }}
          onClick={(event: React.MouseEvent) => {
            event.stopPropagation();
            countScore(event,i);
          }}
        >
          <RateIcon iconId={i + 1} />
        </div>
      ))}
    </div>
  );
};

2.index.scss

  width: auto;
  cursor: pointer;
}
.unSelected {
  position: relative;
  display: inline-block;
  font-size: 30px;
  color: gray;
}
.selected {
  position: relative;
  font-size: 30px;
  display: inline-block;
  font-size: 30px;
  color: #ff9933;
}
.halfSelected {
  position: relative;
  display: inline-block;
  font-size: 30px;
  color: gray;
}
.halfSelected:before {
  font-size: 30px;
  display: block;
  z-index: 1;
  position: absolute;
  top: 0;
  left: 0;
  width: 50%;
  content: attr(data-content);
  overflow: hidden;
  color: #ff9933;
}

3.然后在Iconfont下载你想要的图标就行啦。

效果图

rate.gif

或者你也可以使用文字类来展示:

image.png

😜 温馨提示

使用Iconfont图标时,需要将十六进制编码"&#xe625"转为unicode编码"\ue625"。因为data-xxx接收的是string类型。