一、简介
在 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 随机生成。本文是对于点赞基本探索与实现,希望对读者有所帮助。