vault backup: 2026-04-29 22:19:51
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
---
|
||||
tags: [React, Router, Navigation, Frontend]
|
||||
create time: 2026-04-29 22:07
|
||||
---
|
||||
|
||||
# 路由管理
|
||||
|
||||
## 概述
|
||||
|
||||
前端路由是单页应用(SPA)的核心基础设施。本文档以 React Router v7 为主介绍路由配置、嵌套路由、懒加载、动态路由和守卫拦截等实战模式。
|
||||
|
||||
## React Router v7 核心概念
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[BrowserRouter] --> B["哈希历史 vs HTML5 历史"]
|
||||
B --> C[RouterProvider]
|
||||
C --> D[Route 树]
|
||||
D --> E["Element / Component / ComponentFunction"]
|
||||
D --> F["Layout Route"]
|
||||
F --> G["Child Routes"]
|
||||
|
||||
style A fill:#F4DBD6,color:#000
|
||||
style E fill:#61DAFB,color:#000
|
||||
```
|
||||
|
||||
### 基本结构
|
||||
|
||||
```tsx
|
||||
// app.tsx(v7 推荐入口)
|
||||
import { createRootRouteWithContext, createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
// 或传统 react-router-dom v6/v7
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <RootLayout />,
|
||||
children: [
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: "about", element: <AboutPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Route 与嵌套路由
|
||||
|
||||
```tsx
|
||||
{
|
||||
// /dashboard 及其子路径共用此布局
|
||||
path: "dashboard",
|
||||
element: <DashboardLayout />, // 侧边栏 + Header 常驻
|
||||
children: [
|
||||
{ index: true, element: <DashboardHome /> }, // /dashboard → DashboardHome
|
||||
{ path: "stats", element: <StatsPage /> }, // /dashboard/stats
|
||||
{ path: "settings", element: <SettingsPage /> }, // /dashboard/settings
|
||||
{ path: "settings/:tab", element: <SettingsTab /> }, // /dashboard/settings/profile
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
```jsx
|
||||
// DashboardLayout.tsx
|
||||
function DashboardLayout() {
|
||||
return (
|
||||
<div className="dashboard-layout">
|
||||
<Sidebar />
|
||||
<header><Navigation /></header>
|
||||
<main>
|
||||
<Outlet /> {/* v7 Outlet 替代了 v6的children渲染 */}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] Outlet vs Children
|
||||
> - v6 用 `<Routes>` 内部定义子路由
|
||||
> - v7 用 `<Outlet />` 显式标注嵌套出口,更清晰
|
||||
|
||||
## 路由参数获取
|
||||
|
||||
```tsx
|
||||
import { useSearchParams, useParams, useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
function UserPage() {
|
||||
const { id } = useParams(); // /user/:id → { id: "123" }
|
||||
const [searchParams] = useSearchParams(); // ?q=hello&page=1
|
||||
const location = useLocation(); // { pathname: "/user/123", search: "?q=hello" }
|
||||
const navigate = useNavigate(); // navigate("/home") / navigate(-1)
|
||||
|
||||
const query = searchParams.get("q");
|
||||
|
||||
return <h1>User #{id}, searching for "{query}"</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
### v7 新增:useRouteContext + Data APIs
|
||||
|
||||
```tsx
|
||||
// v7 data routers 支持 loader/action
|
||||
{
|
||||
path: "posts/:postId",
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch(`/api/posts/${params.postId}`);
|
||||
return res.json();
|
||||
},
|
||||
// loader 数据自动注入 component props
|
||||
}
|
||||
```
|
||||
|
||||
## 懒加载 Code Splitting
|
||||
|
||||
```tsx
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
// 路由级代码分割
|
||||
const AdminPage = lazy(() => import("./pages/AdminPage"));
|
||||
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
||||
|
||||
{
|
||||
path: "admin",
|
||||
element: (
|
||||
<Suspense fallback={<PageSpinner />}>
|
||||
<AdminPage />
|
||||
</Suspense>
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 懒加载时机判断
|
||||
> - 首屏路由(首页、登录页)**不要**懒加载
|
||||
> - 低频访问页面(设置、管理员面板)适合懒加载
|
||||
> - 每个 chunk 建议不超过 100KB gzipped
|
||||
|
||||
## 动态路由与 Splat 路由
|
||||
|
||||
```tsx
|
||||
// :param —— 单个段匹配
|
||||
{ path: "users/:userId", element: <UserProfile /> } // /users/42
|
||||
|
||||
// * splat —— 贪婪匹配剩余所有
|
||||
{ path: "docs/*", element: <DocViewer /> } // /docs/a/b/c
|
||||
{ path: "*", element: <NotFoundPage /> } // 404 fallback
|
||||
```
|
||||
|
||||
## 路由守卫与权限控制
|
||||
|
||||
```tsx
|
||||
// 方案1:条件渲染(简单场景)
|
||||
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
const { user } = useAuth();
|
||||
if (!user) return <Navigate to="/login" replace />;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// 方案2:高阶包装
|
||||
const withAuth = (Component: React.FC) => {
|
||||
return (props: any) => {
|
||||
const { loading, authenticated } = useAuth();
|
||||
if (loading) return <Spinner />;
|
||||
if (!authenticated) return <Navigate to="/login" replace />;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
};
|
||||
|
||||
// 方案3:v7 Loader 守卫(服务端前置检查)
|
||||
{
|
||||
path: "admin",
|
||||
loader: () => {
|
||||
if (!isAuthenticated()) throw new Response("", { status: 401 });
|
||||
if (!isAdmin()) throw new Response("", { status: 403 });
|
||||
},
|
||||
element: <AdminPanel />,
|
||||
}
|
||||
```
|
||||
|
||||
## 导航 API 对比
|
||||
|
||||
| 方法 | 适用场景 | 是否保留历史记录 |
|
||||
|------|----------|------------------|
|
||||
| `navigate(path)` | 程序化跳转 | ✅ 有 history entry |
|
||||
| `<Link to="/">Home</Link>` | 声明式导航 | ✅ 预加载 prefetch |
|
||||
| `navigate(-1)` | 返回上一页 | ✅ |
|
||||
| `<Navigate to="/login" replace />` | 替换当前 entry | ❌ 不增加 history |
|
||||
|
||||
## 关联笔记
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
tags: [React, State Management, Context, Zustand, Redux, Frontend]
|
||||
create time: 2026-04-29 22:08
|
||||
---
|
||||
|
||||
# 状态管理
|
||||
|
||||
## 概述
|
||||
|
||||
当组件树变得复杂,跨层级共享状态、服务器数据同步和状态变更追踪成为挑战。本文档对比 Context API、Zustand 和 Redux Toolkit 三种主流方案,帮助你在不同场景下做出正确选择。
|
||||
|
||||
## 选型决策图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["需要全局状态吗?"] -->|"否"| B["useState / useReducer ✅"]
|
||||
A -->|"是"| C["状态类型?"]
|
||||
|
||||
C --> D["纯 UI 状态<br/>(主题、菜单展开、模态框)"]
|
||||
C --> E["服务器数据 / 异步缓存"]
|
||||
C --> F["应用级业务状态<br/>(用户信息、购物车、权限)"]
|
||||
|
||||
D --> G["Context API ✅"]
|
||||
E --> H["TanStack Query / SWR ✅"]
|
||||
F --> I["状态规模?"]
|
||||
|
||||
I --> J["小型 (< 5 store)"]
|
||||
I --> K["中大型"]
|
||||
|
||||
J --> L["Zustand ✅"]
|
||||
K --> M["Redux Toolkit + RTK Query ✅"]
|
||||
|
||||
style G fill:#4FC08D,color:#fff
|
||||
style H fill:#F5A87D,color:#000
|
||||
style L fill:#61DAFB,color:#000
|
||||
style M fill:#764abc,color:#fff
|
||||
```
|
||||
|
||||
## Context API —— 轻量传递
|
||||
|
||||
```tsx
|
||||
const AuthContext = createContext<{ user: User | null; login: (u: User) => void }>({
|
||||
user: null,
|
||||
login: () => {},
|
||||
});
|
||||
|
||||
// Provider
|
||||
function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
const login = (u: User) => setUser(u);
|
||||
|
||||
return <AuthContext.Provider value={{ user, login }}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
// Consumer
|
||||
function Profile() {
|
||||
const { user, login } = useContext(AuthContext);
|
||||
return <div>Hello, {user?.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Context 的性能局限
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| value 变化时所有消费组件重渲染 | Context Value 引用每次都是新的 | 拆分多个 Context / 用 reducer 保持 dispatch 引用稳定 |
|
||||
| 不支持 selector | 没有 "只取子字段" 的机制 | 手动封装或使用第三方库 |
|
||||
| SSR hydration mismatch | 客户端与初始值不一致 | 延迟消费或用 useEffect 包裹 |
|
||||
|
||||
## Zustand —— 轻量级现代方案
|
||||
|
||||
```ts
|
||||
import { create } from "zustand";
|
||||
|
||||
interface StoreState {
|
||||
count: number;
|
||||
users: User[];
|
||||
increment: () => void;
|
||||
fetchUsers: () => Promise<void>;
|
||||
}
|
||||
|
||||
const useStore = create<StoreState>((set, get) => ({
|
||||
count: 0,
|
||||
users: [],
|
||||
|
||||
increment: () => set(state => ({ count: state.count + 1 })),
|
||||
|
||||
fetchUsers: async () => {
|
||||
const res = await fetch("/api/users");
|
||||
const data = await res.json();
|
||||
set({ users: data });
|
||||
},
|
||||
}));
|
||||
|
||||
// 组件中使用
|
||||
function Counter() {
|
||||
// ✅ 只订阅 count —— 其他状态变化不会触发此组件重渲染
|
||||
const count = useStore(s => s.count);
|
||||
const increment = useStore(s => s.increment);
|
||||
|
||||
return <button onClick={increment}>Count: {count}</button>;
|
||||
}
|
||||
|
||||
// 批量更新
|
||||
useStore.setState(({ count }) => ({ count: count + 1, flag: true }));
|
||||
```
|
||||
|
||||
### Zustand 的优势
|
||||
|
||||
| 特性 | Zustand | Context |
|
||||
|------|---------|---------|
|
||||
| Bundle size | ~1KB | 内置 React(~0) |
|
||||
| Selector 支持 | ✅ 精确订阅 | ❌ 全部消费者都重渲染 |
|
||||
| TypeScript 推断 | 完善 | 需手动标注 |
|
||||
| DevTools | 原生支持 | 无 |
|
||||
| 中间件扩展 | persist、immer、devtools | 需额外封装 |
|
||||
|
||||
## Redux Toolkit —— 企业级方案
|
||||
|
||||
```ts
|
||||
import { createSlice, configureStore, useDispatch, useSelector } from "@reduxjs/toolkit";
|
||||
|
||||
interface CounterSlice {
|
||||
value: number;
|
||||
status: "idle" | "loading" | "succeeded" | "failed";
|
||||
}
|
||||
|
||||
const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
initialState: { value: 0, status: "idle" } as CounterSlice,
|
||||
reducers: {
|
||||
incremented: state => { state.value += 1; }, // ✅ Immer:直接 mutate!
|
||||
fetchedAsync: {
|
||||
pending: state => { state.status = "loading"; },
|
||||
fulfilled: (state, action) => {
|
||||
state.status = "succeeded";
|
||||
state.value = action.payload.value;
|
||||
},
|
||||
rejected: state => { state.status = "failed"; },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { incremented, fetchedAsync } = counterSlice.actions;
|
||||
|
||||
const store = configureStore({
|
||||
reducer: { counter: counterSlice.reducer },
|
||||
});
|
||||
```
|
||||
|
||||
```tsx
|
||||
function CounterComponent() {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const count = useSelector((s: AppState) => s.counter.value);
|
||||
|
||||
return <button onClick={() => dispatch(incremented())}>{count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### RTK Query —— 内置数据获取
|
||||
|
||||
```ts
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
|
||||
const api = createApi({
|
||||
reducerPath: "api",
|
||||
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
|
||||
endpoints: build => ({
|
||||
getUsers: build.query<User[], void>({ query: () => "/users" }),
|
||||
updateUser: build.mutation<User, Partial<User>>({
|
||||
query: ({ id, ...patch }) => ({ url: `/users/${id}`, method: "PATCH", body: patch }),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetUsersQuery, useUpdateUserMutation } = api;
|
||||
```
|
||||
|
||||
## 三框架横向对比
|
||||
|
||||
| 维度 | Context API | Zustand | Redux Toolkit |
|
||||
|------|-------------|---------|---------------|
|
||||
| Bundle 大小 | ~0KB(内置) | ~1KB | ~15KB |
|
||||
| 学习曲线 | 低 | 低 | 中 |
|
||||
| Selector | ❌ | ✅ | ✅(useSelector) |
|
||||
| Immutable | ❌ | ❌(但可用 immer middleware) | ✅(Immer 内置) |
|
||||
| 调试工具 | ❌ | ✅ | ✅ Redux DevTools |
|
||||
| 服务端渲染 | ⚠️ 需手动处理 | ✅ | ✅ |
|
||||
| 适用规模 | 小型项目 | 中小/中型 | 大型企业级 |
|
||||
|
||||
## 常见反模式
|
||||
|
||||
> [!warning] 以下做法应避免
|
||||
|
||||
```tsx
|
||||
// ❌ 把所有东西塞进一个 store
|
||||
const useBigStore = create(() => ({
|
||||
user: ..., theme: ..., sidebarOpen: ..., notifications: ..., cart: ..., preferences: ..., // 20+ 个状态
|
||||
}));
|
||||
|
||||
// ✅ 按功能拆分为多个 store
|
||||
const useUserStore = create(...)
|
||||
const useThemeStore = create(...)
|
||||
|
||||
// ❌ 在 state 中存储服务器返回的数据却不做缓存
|
||||
const [data, setData] = useState(fetch(...)) // 页面切换丢失,重复请求
|
||||
|
||||
// ✅ 使用 TanStack Query 等专用数据获取库处理缓存和失效
|
||||
const { data } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
tags: [React, TypeScript, Frontend]
|
||||
create time: 2026-04-29 22:09
|
||||
---
|
||||
|
||||
# TS + React
|
||||
|
||||
## 概述
|
||||
|
||||
TypeScript 为 React 提供编译时类型检查和智能提示。本文档系统梳理 Props、State、Hook、Ref 等核心场景的类型定义方式,以及进阶的泛型推导模式。
|
||||
|
||||
## Props 类型定义
|
||||
|
||||
### 基础方式
|
||||
|
||||
```tsx
|
||||
// 方式1:interface(推荐,可 extend)
|
||||
interface ButtonProps {
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
const Button = ({ label, onClick }: ButtonProps) => <button onClick={onClick}>{label}</button>;
|
||||
|
||||
// 方式2:type alias
|
||||
type ButtonProps = { label: string; onClick?: () => void };
|
||||
|
||||
// ⚠️ 避免:函数参数解构后不再标注
|
||||
// 这样会导致每个参数无法被单独推断
|
||||
function Component({ a, b }) { ... } // any!
|
||||
```
|
||||
|
||||
### 合成事件类型
|
||||
|
||||
```tsx
|
||||
// ❌ 不要用 HTML 原生的 Event
|
||||
const handleChange = (e: Event) => {};
|
||||
|
||||
// ✅ 用 React 的合成事件类型
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.target.value; // string | number | string[]
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
console.log(e.button); // 鼠标按键
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") submit();
|
||||
};
|
||||
```
|
||||
|
||||
### Children 类型
|
||||
|
||||
```tsx
|
||||
// 通用 children 类型
|
||||
function Card({ children }: { children: React.ReactNode }) {}
|
||||
|
||||
// 严格类型 children(限制允许的子节点类型)
|
||||
interface TabsProps {
|
||||
children: React.ReactElement<TabProps>; // 只能是 Tab 组件
|
||||
}
|
||||
|
||||
// 数组形式
|
||||
interface ListProps<T> {
|
||||
children: React.ReactElement<{ item: T }> [];
|
||||
}
|
||||
```
|
||||
|
||||
## State 类型推导
|
||||
|
||||
```tsx
|
||||
interface User { name: string; role: "admin" | "user"; age: number }
|
||||
|
||||
// 完整泛型
|
||||
const [user, setUser] = useState<User>({ name: "", role: "user", age: 0 });
|
||||
|
||||
// 可选初始值
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
// 使用时需判空
|
||||
user?.name; // 安全
|
||||
(user as User).name; // or
|
||||
user!?.name; // non-null assertion
|
||||
|
||||
// useReducer 类型推导
|
||||
interface State { items: Item[]; filter: string }
|
||||
type Action = { type: "SET_FILTER"; payload: string } | { type: "ADD_ITEM"; payload: Item };
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case "SET_FILTER": return { ...state, filter: action.payload };
|
||||
case "ADD_ITEM": return { ...state, items: [...state.items, action.payload] };
|
||||
}
|
||||
}
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
```
|
||||
|
||||
## Ref 类型定义
|
||||
|
||||
```tsx
|
||||
// 元素 ref
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// mutable value ref(不触发 re-render)
|
||||
const timerIdRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
||||
timerIdRef.current = setInterval(() => {}, 1000);
|
||||
|
||||
// class component style ref object(用于挂载子组件引用)
|
||||
const childRef = useRef<ComponentInstance>(null);
|
||||
// 需要 useImperativeHandle 暴露方法给父级
|
||||
|
||||
// forwardRef 中 ref 的正确用法
|
||||
const FancyInput = forwardRef<HTMLInputElement, { label: string }>(function FancyInput(props, ref) {
|
||||
const innerRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => innerRef.current?.focus(),
|
||||
blur: () => innerRef.current?.blur(),
|
||||
}));
|
||||
|
||||
return <input ref={innerRef} label={props.label} />;
|
||||
});
|
||||
```
|
||||
|
||||
## Hook 类型推导
|
||||
|
||||
```tsx
|
||||
// 自定义 Hook 返回值类型
|
||||
interface UseCountReturn {
|
||||
count: number;
|
||||
increment: () => void;
|
||||
decrement: () => void;
|
||||
}
|
||||
|
||||
function useCount(initial = 0): UseCountReturn {
|
||||
const [count, setCount] = useState(initial);
|
||||
return {
|
||||
count,
|
||||
increment: () => setCount(c => c + 1),
|
||||
decrement: () => setCount(c => c - 1),
|
||||
};
|
||||
}
|
||||
|
||||
// generic hook —— 最强大的类型推导场景
|
||||
function useAsync<T, Args extends any[]>(
|
||||
asyncFn: (...args: Args) => Promise<T>,
|
||||
deps: DependencyList
|
||||
): { data: T | null; loading: boolean; error: Error | null; invoke: (...args: Args) => void } {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const invoke = useCallback(async (...args: Args) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await asyncFn(...args);
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [asyncFn, ...deps]);
|
||||
|
||||
useEffect(() => { invoke(); }, [invoke]);
|
||||
|
||||
return { data, loading, error, invoke };
|
||||
}
|
||||
|
||||
// 使用:T 自动推导!
|
||||
const { data: users } = useAsync(fetchUsers, []); // data: User[] | null
|
||||
const { data: user } = useAsync(fetchUserById, [id]); // data: User | null
|
||||
```
|
||||
|
||||
## discriminated Union(区分联合类型)
|
||||
|
||||
```tsx
|
||||
// 类型守卫 —— React 状态管理中最常用的类型模式
|
||||
interface SuccessAction { type: "success"; data: User[] }
|
||||
interface ErrorAction { type: "error"; error: string }
|
||||
interface LoadingAction { type: "loading" }
|
||||
|
||||
type Action = SuccessAction | ErrorAction | LoadingAction;
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case "success":
|
||||
return { ...state, data: action.data, status: "loaded" };
|
||||
// ✅ TS 自动推断 action.data 是 User[]
|
||||
case "error":
|
||||
return { ...state, error: action.error };
|
||||
// ✅ TS 自动推断 action.error 是 string
|
||||
case "loading":
|
||||
return { ...state, status: "loading" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["Union Type"] -->|"switch / if"| B["Type Narrowing"]
|
||||
B --> C["discriminated union: type field"]
|
||||
B --> D["typeof check"]
|
||||
B --> E["in operator"]
|
||||
|
||||
style C fill:#61DAFB,color:#000
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
Reference in New Issue
Block a user