使用 Vit 创建的一个简单的 React 应用程序。
npm create vite@latest
- 用<></>来代替 div,但是这样就不能加 class 了,而且不会在页面结构渲染出来,只是一个占位符
- class 是关键字,应该使用 className 代替
- 遵循组件化的**,将页面拆分成多个组件,每个组件只负责自己的功能,然后再组合起来
- React 组件名称的首字母必须大写
- 组件书写规范
// 有两种书写格式:
// 1. 函数式组件,也叫无状态组件
// 一般在单独的文件中定义时使用
export default function App() {
return (
<div className="App">
<h1>Hello World</h1>
);
}
// 2. 箭头函数式组件,其他情况下使用
const App = () => {
return (
<div className="App">
<h1>Hello World</h1>
);
}
export default App;
6.绑定事件
Button.jsx
:
export default function Button() {
// 1. 通过 onClick 属性绑定事件
// ()=>{} 就是一个匿名函数,当点击按钮时,就会执行这个函数
// return <button onClick={()=>{}}></button>
// 如果只是简单处理,直接在 onClick 中写函数体即可
// return <button onClick={()=>{console.log('点击了按钮')}}></button>
// 复杂处理就要单独定义一个函数,然后在 onClick 中调用
// 2. 事件处理函数的命名规范:handle+事件名
const handleClick = () => {
console.log("点击了按钮1");
console.log("点击了按钮2");
};
return <button onClick={handleClick}></button>;
// 注意:onClick 后面不要加括号,否则会立即执行
}
7.props 传递数据
在 App.jsx 中:
import Button from './Button';
export default function App() {
return (
<div className="App">
<Button text="按钮1" /> {/* 传递数据 */}
)
}
在 Button.jsx 中:
export default function Button(props) {
// {text: "按钮1"},props 是一个对象,里面存储了所有传递过来的数据
console.log(props);
// 使用 props 中的数据
// return <button>{props.text}</button>
// 如果传递的数据比较多,可以使用解构赋值
const { text } = props;
return <button>{text}</button>;
}
甚至可以直接解构赋值:
export default function Button({ text }) {
return <button>{text}</button>;
}
8.使用 React hooks
import { useState } from "react"; // 引入 useState
export default function Button() {
// 1. 定义一个状态
// useState() 接收一个参数,就是状态的初始值
// 返回一个数组,第一个元素是状态,第二个元素是一个函数,用来修改状态
const [count, setCount] = useState(0); // count 是状态,setCount 是修改状态的函数
// 2. 使用状态
return (
// 点击 button 时,修改状态
<button onClick={() => setCount((count) => count + 1)}> {count} </button>
);
}
为什么使用 hook?
React 组件在更新状态时,会重新渲染,如果状态发生变化,就会重新渲染,如果状态没有发生变化,也会重新渲染,这样会影响性能。 使用 hook 可以避免这个问题。
9.useEffect
前面 useSate 是用来定义状态的,而 useEffect 是用来处理副作用的。
useEffect 是一个函数,用来处理副作用,比如:发送网络请求、操作 DOM 等。
格式:useEffect(函数, [依赖的状态])
- useEffect 接收一个函数作为参数,这个函数就是用来处理副作用的。
- 函数中可以写任何代码,比如:发送网络请求、操作 DOM 等。
- 函数中可以返回一个函数,这个函数就是用来清除副作用的。
- useEffect 的第二个参数是一个数组,用来指定依赖的状态,当依赖的状态发生变化时,就会重新执行 useEffect 中的函数。
- 数组为空表示该组件只运行一次
- 不写第二个参数,表示每次状态发生变化时,都会重新执行 useEffect 中的函数。
- [state],表示指定依赖的状态,当依赖的状态发生变化时,就会重新执行 useEffect 中的函数。
import { useState, useEffect } from "react";
export default function Button() {
const [count, setCount] = useState(0);
// 1. 定义一个副作用
useEffect(() => {
console.log("副作用");
// 2. 返回一个函数,用来清除副作用
return () => {
console.log("清除副作用"); // 当组件卸载时,就会执行这个函数
};
}, [count]); // 依赖的状态发生变化时,就会重新执行 useEffect 中的函数
return (
<button onClick={() => setCount((count) => count + 1)}> {count} </button>
);
}
10.React 中的基础 JS
- 为什么返回内容要用小括号包裹?
- 因为 JSX 语法中,只能有一个根元素,如果有多个元素,就需要用小括号包裹。
- 字符串拼接
// 使用 + 号拼接
const age = 18;
const str1 = "年龄:" + age; // 年龄:18
// 【推荐】使用模板字符串拼接
const str2 = `年龄:${age}`; // 年龄:18
- 简单的条件渲染
// 1. if 语句
const value = 5;
if (value > 3) {
return <span>大于3</span>;
}
return <span>{value}</span>;
不推荐上面这么写.
// 推荐使用三元运算符,且只有一个return
const value = 5;
return <span>{value>3 ? '大于3' : value}</span>
// 还有简写的形式,如果条件成立,就会返回后面的内容,
// 前面 false,都不执行后面,称为短路
retrun <span>{value>3 && '大于3'}</span> // 大于3
- 短路&&
- 如果前面的表达式为 true,就会返回后面的内容,如果前面的表达式为 false,就会返回前面的表达式。
- 一般用来做条件渲染。
- 如果前面的表达式为 false,就不会执行后面的表达式,这样可以提高性能。
- 短路||
- 如果前面的表达式为 true,就会返回前面的表达式,如果前面的表达式为 false,就会返回后面的内容。
- 一般用来设置默认值。
- 如果前面的表达式为 true,就不会执行后面的表达式,这样可以提高性能。
注意:假值有以下 6 个: null、 undefined、 NaN、 false、 0、 ''(空字符串)
function add(a, b) {
// 真值短路
// 短路或 || 经常用来设置函数参数的默认值
a = a || 0;
b = b || 0;
console.log(a + b);
}
add(); // 0
add(1); // 1
add(1, 2); // 3
add(100, 200); // 300
- 数组操作
- map():遍历数组,返回一个新的数组,新数组中的元素是回调函数的返回值。
- forEach():遍历数组,没有返回值。
- filter():遍历数组,返回一个新的数组,新数组中的元素是回调函数中返回 true 的元素。
- find():遍历数组,返回第一个满足条件的元素,如果没有满足条件的元素,就返回 undefined。
- some():遍历数组,返回一个布尔值,只要有一个元素满足条件,就返回 true,否则返回 false。
- every():遍历数组,返回一个布尔值,只有所有元素都满足条件,才返回 true,否则返回 false。
const arr = [1, 2, 3, 4, 5];
// 1. map()
const newArr = arr.map((item) => {
return item * 2; // 返回一个新的数组,新数组中的元素是回调函数的返回值
});
console.log(newArr); // [2, 4, 6, 8, 10]
console.log(arr); // [1, 2, 3, 4, 5]
// 2. forEach()
arr.forEach((item) => {
console.log(item); // 1 2 3 4 5
});
// 3. filter()
const newArr2 = arr.filter((item) => {
return item > 3; // 返回一个新的数组,新数组中的元素是回调函数中返回 true 的元素
});
const newArr3 = [arr, 6];
console.log(newArr3); // [Array(5), 6]
const newArr4 = [...arr, 6]; // 展开运算符,也称解构赋值
console.log(newArr4); // [1, 2, 3, 4, 5, 6]
11.React 中的基础 CSS
Count.css
: 此时的样式是全局的,可能影响到其他组件。
若要局部样式,可以修改名称为:Count.module.css
.count {
color: red;
}
Count.jsx
:
import style from './Count.module.css'; // 引入局部样式
exprot default function Count() {
// reutrn <span className="count">0</span>
return <span className={style.count}>0</span> // 使用局部样式
}
但其实上面写法也不推荐,React 推荐使用 Tailwind CSS,一个基于原子类的 CSS 框架。
将在后面项目中介绍使用
- React hook
- useState, useEffect 简单使用
- 以 use 开头的函数被称为 Hook
- 项目整体结构分解组件化
- 嵌套组件
- props 传递数据:
- 父子组件
- 父组件向子组件传递数据
- radix-ui
在 HTML 中,表单元素如 <input>
,<textarea>
和 <select>
表单元素通常保持自己的状态,并根据用户输入进行更新。而在 React 中,可变状态一般保存在组件的 state(状态) 属性中,并且只能通过 setState()
更新。
我们可以通过使 React 的 state
成为 “单一数据源原则” 来结合这两个形式。然后渲染表单的 React 组件也可以控制在用户输入之后的行为。
这种形式,其值由 React 控制的输入表单元素称为“受控组件”。
那么相反的,值并不由 React 进行控制,该组件自己输入,减少等等,该元素成为非受控组件。
- 受控组件:数据由 React 控制( state )
- 非受控组件:数据由 DOM 控制
<textarea>
组件可以渲染一个多行文本输入框
- 可以是非受控组件,使用
defaultValue
参数传递数据 - 可以是受控组件,使用
value
参数传递数据,必须同时传递一个onChange
处理函数,用于更新传递的值 - 一个文本框在其生命周期内无法在受控和非受控之间切换,不能同时既是受控组件又是非受控组件
// 汉字算两个字符,其他都算一个字符
export const countWords = (text) => {
var cn = 0;
if (text.match(/[\u4e00-\u9fa5]/g) != null) {
cn = text.match(/[\u4e00-\u9fa5]/g).length;
}
var total = cn + text.length;
return { total, cn };
};
npx create-next-app@latest
basics-nextjs
│ .eslintrc.json
│ .gitignore
│ next-env.d.ts
│ next.config.mjs
│ package-lock.json
│ package.json
│ postcss.config.mjs
│ README.md
│ tailwind.config.ts
│ tsconfig.json
│
├─app
│ favicon.ico
│ globals.css
│ layout.tsx
│ page.tsx
│
└─public
next.svg
vercel.svg
结构说明:
- app: 存放全局样式和布局组件
page.tsx
是页面组件layout.tsx
是布局组件,一些可以共用的组件,比如:导航栏、页脚等。page.tsx
中的<Layout>
标签就是布局组件,可以将<Layout>
标签中的内容放到layout.tsx
中的{children}
中。globals.css
是全局样式,可以在里面定义一些全局样式,比如:body
、a
等。
- public: 存放静态资源,比如图片、字体等
- components: 存放公共组件
- styles: 存放样式文件,比如:
index.module.css
,about.module.css
- utils: 存放工具函数
- lib: 存放第三方库,比如:
axios
- types: 存放类型定义文件,比如:
index.d.ts
- 动态路由:
/posts/[id].tsx
[]
中的内容是动态的,比如:[id]
,就表示 id 是动态的,可以是任意值。/posts/[...id]
,...
表示可以有多个参数,比如:[...id]
,就表示 id 可以是多个参数,比如:1/2/3
,就表示 id 是 1、2、3。
- 分组路由:
/(auth)/...
()
中的内容是分组的,比如:(auth)
,就表示 auth 是分组的,可以是任意值。
- 并行路由:可以根据不同用户角色显示不同内容
/@admin/...
/@user/...
- 拦截路由:允许在当前布局中从应用程序的另一部分加载路由,当您想在不切换到其他上下文的情况下显示路由的内容时使用
- (.) to match segments on the same level
- (..) to match segments one level above
- (..)(..) to match segments two levels above
- (...) to match segments from the root app directory
- SSR(Server-side Rendering):服务端渲染,又称动态渲染,首屏直出,SEO 友好
- 需要导出一个名为 getServerSideProps 的异步函数。服务器将在每次请求时调用此函数
- 每次请求时都会生成页面 HTML
- CSR(Client-side Rendering):客户端渲染,又称静态渲染,首屏不直出,不 SEO 友好
- 仅在客户端渲染,首屏不直出,不 SEO 友好
- 通过
useEffect
来获取数据,然后渲染页面 - 通过
useRouter
来获取路由对象,然后通过router.query
来获取路由参数
- SSG(Static Site Generation):静态网站生成,又称预渲染,首屏直出,SEO 友好
- 需要导出一个名为 getStaticProps 的异步函数。服务器将在构建时调用此函数
- 仅在构建时(
next build
)生成 HTML
- ISR(Incremental Static Regeneration):增量静态再生,又称增量预渲染,首屏直出,SEO 友好
- 需要导出一个名为 getStaticProps 的异步函数。服务器将在构建时调用此函数
- 通过
revalidate
参数来设置多久重新生成页面,单位秒 - 仅在构建时生成 HTML,然后在每次请求时,如果页面过期,就会重新生成 HTML,然后返回给客户端。
- 如果页面没有过期,就会直接返回缓存的 HTML,不会重新生成 HTML。
- 如果页面过期,但是有多个请求,只会有一个请求重新生成 HTML,其他请求会等待,直到重新生成 HTML 完成,然后再返回给客户端。
SSG 无数据:
// 不需要获取预处理数据,在nextjs构建时就生成HTML页面
function About() {
return <div>About</div>;
}
export default About;
SSG 有数据:
export default function Blog({ posts }) {
// Render posts...
}
// This function gets called at build time
export async function getStaticProps() {
// Call an external API endpoint to get posts
const res = await fetch("https://.../posts");
const posts = await res.json();
// By returning { props: { posts } }, the Blog component
// will receive `posts` as a prop at build time
return {
props: {
posts,
},
};
}
ISR
// 就是在SSG的基础上,增加了revalidate参数,用来设置多久重新生成页面,单位秒
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
const res = await fetch("https://.../posts");
const posts = await res.json();
return {
props: {
posts,
},
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every 10 seconds
revalidate: 10, // In seconds
};
}
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
const res = await fetch("https://.../posts");
const posts = await res.json();
// Get the paths we want to pre-render based on posts
const paths = posts.map((post) => ({
params: { id: post.id },
}));
// We'll pre-render only these paths at build time.
// { fallback: 'blocking' } will server-render pages
// on-demand if the path doesn't exist.
return { paths, fallback: "blocking" };
}
export default Blog;
使用 Neon Postgres 和 drizzle-orm
1.安装依赖:
npm i drizzle-orm @neondatabase/serverless
# -D 表示只在Dev环境安装
npm i -D drizzle-kit
# 管理环境变量
npm i dotenv
注意:可能出现的问题
$ npm i drizzle-orm @neondatabase/serverless
npm error code ERESOLVE
npm error ERESOLVE could not resolve
npm error
npm error While resolving: 03.basics-nextjs@0.1.0
npm error Found: react@18.3.1
npm error node_modules/react
npm error peer react@"^18.2.0" from next@14.2.5
npm error node_modules/next
npm error next@"14.2.5" from the root project
npm error peer react@"^18.3.1" from react-dom@18.3.1
npm error node_modules/react-dom
npm error peer react-dom@"^18.2.0" from next@14.2.5
npm error node_modules/next
npm error next@"14.2.5" from the root project
npm error react-dom@"^18" from the root project
npm error 2 more (styled-jsx, the root project)
npm error
npm error Could not resolve dependency:
npm error drizzle-orm@"*" from the root project
npm error
npm error Conflicting peer dependency: react@18.2.0
npm error node_modules/react
npm error peer react@"18.2.0" from react-native@0.74.3
npm error node_modules/react-native
npm error peer react-native@">0.73.0" from @op-engineering/op-sqlite@6.2.11
npm error node_modules/@op-engineering/op-sqlite
npm error peerOptional @op-engineering/op-sqlite@">=2" from drizzle-orm@0.32.0
npm error node_modules/drizzle-orm
npm error peerOptional @op-engineering/op-sqlite@">=2" from drizzle-orm@0.32.0
npm error node_modules/drizzle-orm
npm error drizzle-orm@"*" from the root project
npm error node_modules/drizzle-orm
npm error drizzle-orm@"*" from the root project
npm error drizzle-orm@"*" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
解决方法:
# 更新npm
npm update --save
npm update --save-dev
2.配置环境变量
在根目录下创建.env
文件,然后添加如下内容:
DATABASE_URL=postgres://postgres:123456@localhost:5432/postgres
3.创建数据库表
schema.ts
:
export const usersTable = pgTable("users_table", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
age: integer("age").notNull(),
email: text("email").notNull().unique(),
});
4.配置 drizzle
drizzle.config.ts
import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";
config({ path: ".env" });
export default defineConfig({
schema: "./server/schema.ts",
out: "./server/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
5.生成数据库表
# 生成DB表操作语句
npx drizzle-kit generate --config ./server/drizzle.config.ts
# 迁移
npx drizzle-kit migrate --config ./server/drizzle.config.ts
# 推送
npx drizzle-kit push --config ./server/drizzle.config.ts
6.使用数据库
import { db } from "./db";
import { InsertUser, usersTable } from "./schema";
export async function createUser(data: InsertUser) {
await db.insert(usersTable).values(data);
}
7.使用页面查看数据表
需要再开一个终端执行,不可关闭
npx drizzle-kit studio --config ./server/drizzle.config.ts
Drizzle Studio is up and running on https://local.drizzle.studio
Tailwind CSS 的工作原理是扫描所有 HTML 文件、JavaScript 组件以及任何 模板中的 CSS 类(class)名,然后生成相应的样式代码并写入 到一个静态 CSS 文件中。
他快速、灵活、可靠,没有运行时负担
1.安装:
# NextJS 自带,无需安装
npm install -D tailwindcss
2.配置文件:
/** @type {import('tailwindcss').Config} */
// 配置文件中添加所有模板文件的路径。
module.exports = {
content: ["./src/**/*.{html,js}"],
theme: {
extend: {},
},
plugins: [],
};
3.将加载 Tailwind 的指令添加到 CSS 文件中
在你的主 CSS 文件中通过 @tailwind 指令添加每一个 Tailwind 功能模块。
@tailwind base;
@tailwind components;
@tailwind utilities;
4.开启 Tailwind CLI 构建
npx tailwindcss -i ./src/input.css -o ./src/output.css --watch
5.安装 shadcn-ui
npx shadcn-ui@latest init
# 添加一个按钮测试
npx shadcn-ui@latest add button
useTransition
是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。
const [isPending, startTransition] = useTransition();
使用: 在组件顶层调用 useTransition,将某些状态更新标记为 transition。
import { useTransition } from "react";
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ……
}
useTransition 返回一个由两个元素组成的数组:
- isPending,告诉你是否存在待处理的 transition。
- startTransition 函数,你可以使用此方法将状态更新标记为 transition
quickstart: https://www.prisma.io/docs/getting-started/quickstart 1.安装:
npm install prisma --save-dev
npm i @prisma/client
2.初始化:
npx prisma init
3.编写模型:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
4.生成模型:
npx prisma generate
5.连接数据库:
npx prisma db push
# 查看网页版,http://localhost:5555
npx prisma studio
注意:该操作会覆盖数据库,慎用!
6.使用:
https://authjs.dev 前面已经安装 prisma ,这里需要安装对应的依赖
安装依赖:
npm install @auth/prisma-adapter
# hash 密码
npm i bcrypt
# 安装类型声明
npm i @types/bcrypt -D
#########################
# 也可以使用 bcryptjs
npm i bcryptjs
# 安装类型声明
npm i @types/bcryptjs -D
中间件允许您在请求完成之前运行代码。然后根据传入的请求,可以通过重写、重定向、修改请求或响应标头或直接响应来修改响应。
中间件函数可以执行以下操作:
- 身份验证和授权:在授予对特定页面或 API 路由的访问权限之前,确保用户身份并检查会话 Cookie。
- 服务器端重定向:根据特定条件(例如,区域设置、用户角色)在服务器级别重定向用户。
- 路径重写:通过基于请求属性动态重写 API 路由或页面的路径,支持 A/B 测试、功能卷展栏或遗留路径。
- 机器人检测:通过检测和阻止机器人流量来保护您的资源。
- 日志记录和分析:在页面或 API 处理之前,捕获并分析请求数据以获取见解。
- 功能标记:动态启用或禁用功能,以实现无缝的功能推出或测试。
中间件文件
// 根目录下创建 middleware.ts
import { auth } from "@/auth";
export default auth((req) => {
// req.auth
});
// Optionally, don't invoke Middleware on some paths
// 规则匹配上的路径,中间件不会被调用
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
- ! 是逻辑非运算符,用于将一个值转换为布尔值,如果值为 true,则返回 false,如果值为 false,则返回 true。
- 可以将变量转换为布尔值
- null、undefined、0、NaN、''、false 会被转换为 false,其他值都为 true。
- !! 是双重逻辑非运算符,用于将一个值转换为布尔值,如果值为 true,则返回 true,如果值为 false,则返回 false。
- 常用来判断一个值是否为空,如果值为空,则返回 false,否则返回 true。
注意:视频最后一节 Settings 未完成。
1.安装 NextJS:
npx create-next-app@latest
2.安装 ShadcnUI
npx shadcn-ui@latest init
3.安装 Kinde-Auth
npm i @kinde-oss/kinde-auth-nextjs
使用:
import {
RegisterLink,
LoginLink,
} from "@kinde-oss/kinde-auth-nextjs/components";
<RegisterLink className="flex-1">
<Button className="w-full" variant={"outline"} disabled={loading}>
注册
</Button>
</RegisterLink>;
4.安装黑暗模式
npm install next-themes
创建一个 provider
components/theme-provider.tsx
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
在 root layout 中使用,包裹 {children} 即可
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
在 TypeScript React 文件中,代码片段 [&>div:not(:first-child)]:mt-8
是一个 CSS 选择器和一个 Tailwind CSS 样式规则。
&
符号是对父选择器的引用。在本例中,它指的是父 div 元素,该元素具有类columns-1 sm:columns-2 md:columns-3 lg:columns-4 gap-4
。>
符号是子选择器,它选择直接子元素。div
是 div 元素的元素选择器。:not(:first-child)
是一个伪类,它选择所有不是其父元素的第一个子元素的 div 元素。:mt-8
是一个 Tailwind CSS 实用类,它应用一个上外边距 (margin-top) 为 32px (1rem = 16px, 所以 8 * 16px = 128px)。
因此,完整的选择器 [&>div:not(:first-child)]:mt-8
应用了 mt-8
实用类到父 div 元素的所有直接 div 子元素中,但不包括第一个子元素。这将在每对 div 元素之间产生一个垂直间隙 (vertical gap) 32px。
翻译成中文:在 TypeScript React 文件中,代码片段 [&>div:not(:first-child)]:mt-8
是一个 CSS 选择器和一个 Tailwind CSS 样式规则。它对父 div 元素的所有直接 div 子元素(不包括第一个子元素)应用了 mt-8
实用类,从而在每对 div 元素之间产生一个垂直间隙 (vertical gap) 32px。
// 第一步:
// 鼠标悬停时获取图片index
const [hoverIndex, setHoverIndex] = useState<number | null>(null);
// 鼠标悬停的位置
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
// 第二步:
// 实现的效果:鼠标悬停在图片时,图片跟随鼠标进行局部放大
const handleMouseMove = (
e: React.MouseEvent<HTMLDivElement>,
index: number
) => {
if (hoverIndex === index) {
// react 是鼠标悬停时当前的 DOMRect对象,用于获取元素的大小及其相对于视口的位置
const react = (e.target as HTMLDivElement).getBoundingClientRect();
// 计算鼠标的位置
// react.left:指当前对象的左边距;react.top:指当前对象的上边距
// react.width,react.height 指当前元素的宽,高
// event.clientX - react.left:指鼠标距离当前元素的左边的距离
// x,y 值用于确定在 CSS 类中应用的 transform 属性的 translateX,translateY 值
// 从而实现图片在水平和垂直方向上的局部移动。
const x = ((e.clientX - react.left) / react.width) * 100;
const y = ((e.clientY - react.top) / react.height) * 100;
// 设置值
setMousePosition({ x, y });
}
};
// 第三步:使用
<div
key={i}
className="relative overflow-hidden rounded-md"
onMouseEnter={() => setHoverIndex(i)}
onMouseLeave={() => setHoverIndex(null)}
onMouseMove={(e) => handleMouseMove(e, i)}
>
{/* hover:scale-150 : 鼠标悬停放大1.5倍 */}
{/* transition-transform duration-500 ease-in-out :过渡效果,持续时间,进、出过渡 */}
<Image
src={`/featured/featured${i + 1}.jpg`}
className="cursor-pointer hover:scale-150 transition-transform duration-500 ease-in-out"
alt="Featured Horse"
style={{
transformOrigin: `${mousePosition.x}% ${mousePosition.y}%`,
}}
width={500}
height={500}
/>
</div>;
实现滚动留言部分 https://magicui.design
安装:
npx magicui-cli add marquee
配置 tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
animation: {
marquee: "marquee var(--duration) linear infinite",
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
},
keyframes: {
marquee: {
from: { transform: "translateX(0)" },
to: { transform: "translateX(calc(-100% - var(--gap)))" },
},
"marquee-vertical": {
from: { transform: "translateY(0)" },
to: { transform: "translateY(calc(-100% - var(--gap)))" },
},
},
},
},
};
使用:Testimonials.tsx