基于 Remix 实现一个简单的点赞 ❤️效果

一、简介

在 ToC 的项目中,点赞功能是一个常用的的功能,在主流视频网站中,点赞效果做的十分优秀。

二、选择技术与效果实现

  • 基于 Remix 的 React 框架
  • css module 编写样式
  • 仅仅是前端实现,没有对接后端接口
  • 移动端点赞效果
  • PC 端的点赞效果

三、创建项目安装依赖

ts 复制代码
npx create-remix app
pnpm add uuid

四、样式重置

  • global.css
ts 复制代码
* {
  padding: 0;
  margin: 0;
}

root 中引入重置样式

ts 复制代码
import css from '~/styles/global.css'

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: css },
  ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
];

五、辅助函数

5.1) 辅助函数:emoji 随机生成

ts 复制代码
export function getRandomEmoji() {
  // 随机选择一个起始点(Unicode 范围中的值)
  const start = 0x1F600; // 起始点
  const end = 0x1F64F; // 结束点

  // 生成随机的 Unicode 编码
  const randomCodePoint = Math.floor(Math.random() * (end - start + 1)) + start;

  // 将 Unicode 编码转换为对应的 emoji 并返回
  return String.fromCodePoint(randomCodePoint);
}

从 emoji 的Unicode 范围中获取一个随机的 emoji 代替抖音中图像。

5.2) 辅助函数:特定时间内记录次数

在特点时间(好比 300ms)内执行记录一次,调用一次加 1,如果 300 ms 内,超过 10 次,输出 x10。当最有一次点击超过 300ms 后,没有重新点击,次数归 0。

ts 复制代码
export function huntCount(instance: MangerHunterCount, time: number) {
  let timer: any;

  return function () {
    if (timer) {
      clearTimeout(timer);

      timer = setTimeout(() => {
        console.log("reset");
        instance.reset();
        timer = null;
      }, time);
    } else {
      timer = setTimeout(() => {
        console.log("reset");
        instance.reset();
        timer = null;
      }, time);
    }

    instance.addOne();
  };
}

export class MangerHunterCount {
  count: number;
  constructor() {
    this.count = 0;
  }

  addOne() {
    this.count += 1;
  }

  reset() {
    this.count = 0;
  }
}

六、移动端

6.1) 逻辑

  • 移动端在点击屏幕位置有 pop 弹出效果,同时又连击数字效果(连击超过 10 次)。
  • 移动端点击屏幕,点赞效果从右下角底部连续点赞连续 pop 出来,然后隐藏。

6.2) TSX 实现

ts 复制代码
import type { MetaFunction } from "@remix-run/node";
import { useState } from "react";
import styles from "~/styles/index.module.css";
import * as uuid from "uuid";
import { getRandomEmoji } from "~/utils/index";

export const meta: MetaFunction = () => {
  return [
    { title: "New Remix App" },
    { name: "description", content: "Welcome to Remix!" },
  ];
};

export default function Index() {
  const [list, setList] = useState<any>([]);

  return (
    <div className={styles.page}>
      <h1>Welcome to Remix</h1>
      <div className={styles.wrap}>
        <div className={styles.listWrap}>
          {list.map((li: any) => {
            return (
              <div
                onAnimationEnd={() => {
                  setList((pre: any) => pre.filter((l: any) => l.id !== li.id));
                }}
                key={li.id}
                className={styles.emoji}
              >
                {li.content}
              </div>
            );
          })}
        </div>
        <div
          className={styles.heart}
          onClick={() => {
            setList([
              ...list,
              { id: uuid.v4(), content: getRandomEmoji() || "⛄" },
            ]);
          }}
        >
          ❤️
        </div>
      </div>
    </div>
  );
}
  • list 列表维护点赞的 pop。
  • ❤️ 点击之后,使用 uuid 添加随机的 emoji 的图像。
  • 当 pop 动画结束之后,更具 uuid 过滤对应的值, 直到列表清空。

6.3) CSS

css 复制代码
.page {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  height: 90vh;
  cursor: pointer;
  background-color: antiquewhite;
}

.wrap {
  position: relative;
  display: flex;
  margin-top: 300px;
}

.listWrap {
  position: relative;
}

.heart {
  position: relative;
  top: 0px;
  display: flex;
  cursor: pointer;
  font-size: 30px;
}

.heart:active {
  position: relative;
  top: 0px;
  display: flex;
  cursor: pointer;
  font-size: 30px;
  transform: scale(1.2);
}

.emoji {
  position: absolute;
  animation-duration: 3s;
  animation-name: pop;
  animation-fill-mode: forwards;
  cursor: pointer;
  font-size: 30px;
}

@keyframes pop {
  from {
    transform: translateY(-30px) scale(1);
    opacity: 0.6;
  }

  50% {
    transform: translateY(-200px) scale(2);
    opacity: 1;
  }
  to {
    transform: translateY(-300px) scale(1);
    opacity: 0;
  }
}

css 中定义了 pop 动画,这个动画在结束时使用 forwards 属性控制在最后一帧。

七、PC 端

7.1) 逻辑

  • 没有连击效果
  • PC 端与移动的效果不一样,pop 的出现位置时 PC 端点击时,根据点击位置确定的
  • Pop 效果是进入变大,突然变小,然后渐进式变大(同时透明度变低,然后消失)
  • PC 端在连续点击 10 次以上,会显示 x10 字样提示
  • 连续点击监听 dbclick 不在合理,应该自己模拟 dbclick 点击事件

7.2) TSX

ts 复制代码
import type { MetaFunction } from "@remix-run/node";
import { useEffect, useState } from "react";
import styles from "~/styles/client.pc.module.css";
import * as uuid from "uuid";
import { getRandomEmoji } from "~/utils/index";
import { MangerHunterCount, huntCount } from "~/utils/hunt-count";

const inst = new MangerHunterCount();
const fn = huntCount(inst, 2000);
let clicks = 0;
let timer: any = null;

export const meta: MetaFunction = () => {
  return [
    { title: "New Remix App" },
    { name: "description", content: "Welcome to Remix!" },
  ];
};

export default function Index() {
  const [list, setList] = useState<any>([]);

  function handleClick(e: any)  {
    const { clientX, clientY } = e;

    clicks++;
  if (clicks === 1) {
    timer = setTimeout(function() {
      // 单击事件
      clicks = 0;
      // 在这里执行点赞操作
      console.log("单击点赞");
    }, 300); // 设置延迟时间,这里是300毫秒
  } else {
    clearTimeout(timer);
    // 双击事件
    clicks = 0;
    fn();



    setList((pre: any) => {
      return [
        ...pre,
        {
          id: uuid.v4(),
          content: getRandomEmoji(),
          x: clientX - 10,
          y: clientY - 10,
          count: inst.count,
        },
      ];
    });
  }

    
  }

  useEffect(() => {
    window.addEventListener("click", handleClick);
    // eslint-disable-next-line react-hooks/exhaustive-deps
    return () => {
      window.removeEventListener("click", handleClick)
    }
  }, []);
  return (
    <div className={styles.page}>
      {/* <h1>Welcome to Remix</h1> */}
      <div className={styles.wrap}>
        <div className={styles.listWrap}>
          {list.map((li: any) => {
            return (
              <div
                onAnimationEnd={() => {
                  setList((pre: any) => pre.filter((l: any) => l.id !== li.id));
                }}
                key={li.id}
                style={{
                  top: li.y,
                  left: li.x,
                }}
                className={styles.emoji}
              >
                {li.content}
                <span className={styles.count}>
                  {li.count >= 10 ? "x" + li.count : null}
                </span>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

7.3) css

ts 复制代码
.page {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  height: 90vh;
  cursor: pointer;
  background-color: antiquewhite;
}

.wrap {
  position: relative;
  display: flex;
  width: 100%;
  height: 500px;
}

.listWrap {
  position: relative;
  display: flex;
  background-color: aqua;
}

.emoji {
  position: absolute;
  animation-name: popnew;
  animation-duration: 0.9s;
  animation-fill-mode: forwards;
  animation-timing-function: ease-in-out;
  cursor: pointer;
  font-size: 30px;
  user-select: none;
}

@keyframes popnew {
  from {
    transform: scale(2);
    opacity: 0.08;
  }

  15% {
    transform: scale(1);
    opacity: 1;
  }

  80% {
    transform: translateY(-20px) scale(1) rotate(30deg);
    opacity: 1;
  }
  to {
    transform:  translateY(-100px) scale(5);
    opacity: 0;
  }
}

.count {
  position: absolute;
  top: -20px;
  color: #fff;
  font-size: 18px;
}

八、小结

本文主要实现基于 React 的前端点赞效果,涉及不少的知识点, css 动画与过渡,控制 css 动画的行为,emoji 随机生成。本文是对于点赞基本探索与实现,希望对读者有所帮助。

相关推荐
代码小鑫2 分钟前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
Json____7 分钟前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
迂 幵15 分钟前
vue el-table 超出隐藏移入弹窗显示
javascript·vue.js·elementui
上趣工作室20 分钟前
vue2在el-dialog打开的时候使该el-dialog中的某个输入框获得焦点方法总结
前端·javascript·vue.js
家里有只小肥猫20 分钟前
el-tree 父节点隐藏
前端·javascript·vue.js
fkalis21 分钟前
【海外SRC漏洞挖掘】谷歌语法发现XSS+Waf Bypass
前端·xss
monkey_meng27 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss35 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
zxg_神说要有光1 小时前
自由职业第二年,我忘记了为什么出发
前端·javascript·程序员
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust