黑马React18学习笔记(下)

一、Redux

1、什么是Redux

Redux是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行。

作用:通过集中管理的方式管理应用的状态。

2、快速体验Redux

需求: 不和任何框架绑定,不适用任何构建工具,使用纯Redux实现计数器。

 使用步骤:

① 定义一个reducer函数(根据当前想要做的修改返回一个新的状态)

② 使用createStore方法传入reducer函数,生成一个store实例对象

③ 使用store实例的subscribe方法订阅数据的变化(数据一旦变化,可以得到通知)

④ 使用store实例的dispatch方法提交action对象触发数据变化(告诉reducer你想怎么改数据)

⑤ 使用store实例的getState方法获取最新的状态数据更新到视图中。

代码:

<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>

<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>


<script>
//1、定义reducer函数
//作用:根据不同的action对象,返回不同的state
//state:管理的数据初始对象
//action:对象type标记当前想要做什么样的修改
fucntion reducer (state = {count :0 } ,action) {
  //数据不可变:基于原始状态生成一个新状态
  if(action.type === 'INCREMENT') {
    return { count:state.count + 1 }
  }
  if(action.type === 'DECREMENT'){
   return { count:state.count - 1 }
  }
  return state
}


//2、使用reducer函数生成store实例
const store = Redux.createStore(reducer)

//3、通过store实例的subcribe订阅数据变化
//回调函数可以在每次state发送变化的时候自动执行
store.subscribe(() => {
  console.log('state变化了',store.getState())
  document.getElementById('count').innerText = store.getState().count;
} )

//4、通过store实例的dispatch函数提交action更改状态
const isBtn = document.getElementById('increment')
isBtn.addEventListener('click',()=>{
//增加
store.dispatch({
  'type':'INCREMENT',
  })
})


const isBtn = document.getElementById('crement')
isBtn.addEventListener('click',()=>{
//减少
store.dispatch({
  'type':'INCREMENT',
  })
})

//5、通过store实例的getState方法获取最新状态更新到视图中


</script>

3、Redux管理数据流程梳理

为了职责清晰,数据流向明确,Redux把整个数据修改的流程分为了三个核心概念,分别是:state、action和reducer。

①、state:一个对象,存放着我们管理的数据状态

②、action:一个对象,用来描述你想怎么改数据

③、reducer:一个函数,根据action的描述生成一个新的state

4、Redux与React环境准备

4.1 配套工具

     在React中使用Redux,官方要去安装两个其他插件:Redux Toolkitreact-redux

     ① Reux Toolkit(RTK):官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式。

 

     ② react-redux:用了链接Redux和React组件的中间件。

 4.2 配置基础环境

① 使用CRA快速创建react项目

npx create-react-app react-redux

② 安装redux

npm i --save redux

③ 安装配套工具

npm i @reduxjs/toolkit react-redux

④ 启动项目

npm run start

4.3 store目录结构设计

① 通常集中状态管理的部分都会单独创建一个单独的store目录。

② 应用通常会有很多个子store模块,所以创建一个modules目录,在内部编写业务分类的子store。

③ store中的入口文件index.js的作用是组合modules中所有的子模块,并导出store。

5、实现counter

5.1 使用React  Toolkit创建counterStore

 5.2 为React注入store

      react-redux负责把Redux和React链接起来,内置Provider组件,通过store参数把创建好的store实例注入到应用中,链接正式建立。

5.3 React组件使用store中的数据

在React组件中使用store中的数据,需要用到一个钩子函数 useSelector,它的作用是把store中的数据映射到组件中,使用样例如下:

const { count } = useSelector(state => state.counter)

其中state.counter来自于创建根store组合子模块里的counter。

import { configureStore } from "@reduxjs/toolkit";
//导入子模块reducer
import counterStore from "../store/modules/counterStore";

//创建根store组合子模块
const store = configureStore({
    reducer:{
     counter:counterStore
    }
})

export default store

 

5.4 React组件修改store中的数据

      React组件中修改store中的数据需要借助另外一个hook函数: useDispatch,它的作用是生成提交action对象的dispatch函数,使用样例如下:    

6、提交action传参

需求说明:

组件中有两个按钮:add to 10 和 add to 20 可以直接把count的值修改到对应的数字,目标count值是在组件中传递过去的,需要在提交action的时候传递参数

实现步骤:

       在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传参,参数会传递到action对象payload属性上。

7、异步状态操作

 ① 创建store的写法保持不变,配置好同步修改状态的方法。

 ② 单独封装一个函数,在函数内部return一个新函数,在新函数中:

      封装异步请求获取数据

      调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交

 ③ 组件中dispatch的写法保持不变。

    

 8、Redux调试——devtools

Redux 官方提供了针对Redux的调试工具,支持实时state信息展示,action提交信息查看等。

调试工具下载链接:Redux DevTools_3.1.6_Chrome插件下载_极简插件 (zzzmh.cn)

9、美团案例

 需求:

基本开发思路:

        使用RTK(Redux Toolkit)来管理应用状态,组件负责数据渲染和dispatch action。

9.1 开发环境准备

       ① 克隆项目到本地(内置基础静态组件和模块)

            git clone http://git.itcast.cn/heimaqianduan/redux-meituan.git

        ② 安装所有依赖

             npm  i

        ③ 启动mock服务(内置了json-server)

               npm  run serve

        ④ 启动前端服务

              npm  run start

9.2 分类和商品列表渲染

      需求:       

实现步骤:

   ①  启动项目(mock服务+前端服务)

   ② 使用RTK编写store(异步action)

   ③ 组件触发action并且渲染数据

9.3 点击分类激活和互助实现

需求:

 实现步骤:

        ① 使用RTK编写管理activeIndex

        ② 组件中点击时触发action更改activeIndex

        ③ 动态控制激活类名显示

 9.4 商品列表切换显示

需求:

9.5 添加购物车实现

需求:

 实现步骤:

        ① 使用RTK管理状态cartList

        ② 如果添加过,只更新数量count,没有添加过,直接push进去

        ③ 组件中点击时收集数据提交action添加购物车

9.6 统计区域功能实现

需求:

实现步骤:

       ① 基于store中的cartList的length渲染数量

       ② 基于store中的cartList累加price*count

       ③ 购物车cartList的length不为0则高亮 

9.7 购物车列表功能实现

需求:

实现步骤:

       ① 使用cartList遍历渲染列表

       ② RTK中增加增减reducer,组件中提交action 

       ③ RTK中增加清除购物车reducer,组件中提交action 

9.8 控制购物车显示和隐藏

需求:

实现步骤: 

      ① 使用useState声明控制显隐的状态

      ② 点击统计区域设置状态为true

      ③ 点击蒙层区域设置状态为false

二、 ReactRouter

1、准备工作

1.1 什么是前端路由

一个路由path对应一个组件component,当我们浏览器中访问一个path的时候,path对应的组件会在页面中进行渲染:

 const router = [
    {
      path: "/about",
      component: About,
    },
    {
      path: "/article",
      component: Article,
    },
  ];

1.2 创建路由开发环境

使用路由我们还是采用CRA创建项目的方式进行基础环境配置。

① 创建项目并安装所有依赖

     npx create-react-app  react-router-pro

② 安装最新的ReactRouter包

     npm i react-router-dom

③ 启动项目

     npm run start

2、抽象路由模块

实际开发中的router配置

 

 

3、路由导航跳转

路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信。

 3.1 声明式导航

声明式导航:通过在模板中通过‘<Link /> 组件描述出要跳转到哪里去’,比如后台管理系统的左侧菜单通常使用这种方式进行。

语法说明:通过给组件的to属性指定要跳转到路由path,组件会渲染为浏览器支持的a链接,如果需要传参直接 通过字符串拼接的方式拼接参数即可。

3.2 编程式导航

编程式导航:通过'useNavigate’钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在登录请求完毕之后跳转就可以选择这种方法,更加灵活。

语法说明:通过调用navigate方法传入地址path实现跳转。

4、导航跳转传参

 

 

5、嵌套路由配置

在一级路由中又嵌套了其他路由,这种关系就叫嵌套路由,嵌套至一级路由内的路由又称作二级路由。

实现步骤:

     ①  使用 children 属性配置路由嵌套关系

     ② 使用 '<Outlet />' 组件配置二级路由渲染位置

6、默认二级路由配置

当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path设置index属性值为true

 

7、404路由配置

场景:当浏览器输入url的路径在整个路由配置中都找不到对应的path,为了用户体验,可以使用404兜底组件进行渲染。

实现步骤:

       ① 准备一个NotFount组件

       ② 在路由表数组的末尾,以*号作为路由path配置路由

8、两种路由模式

        各个主流框架的路由常用的路由模式有两种,history模式和hash模式,ReactRouter分别由createBrowerRouter和createHashRouter函数负责创建。

9、案例:记账本

9.1 环境搭建

使用CRA创建项目,并安装必要依赖,包括下列基础包:

①  Redux状态管理: @reduxjs/tookit、react-redux

②  路由:react-router-dom

③ 时间处理:dayjs

④ class类名处理:classnames

⑤ 移动端组件库:antd-mobile

⑥ 请求插件:axios

9.2 配置别名路径

①、路径解析配置(webpack),把@/解析为src/

②、路径联想配置(VsCode),VsCode在输入@/

9.2.1 路径解析配置

         CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件 -craco

        ①、安装craco

             npm i -D @craco/craco

        ②、项目根目录下创建配置未见

              craco.config.js

        ③、配置文件中添加路径解析配置

        ④、包文件中配置启动和打包命令

9.2.2 联想路径配置 

         VsCode的联想配置,需要我们在项目目录下添加jsconfig.json文件,加入配置之后VsCode会自动读取配置帮助我们自动联想提示。

①、根目录下新增配置文件:jsconfig.json

②、添加路径提示配置

9.3  数据Mock实现

9.3.1 什么是数据Mock

         在前后端分离的开发模式下,前端可以在没有实际后端接口的支持下先进行接口数据的模拟,进行正常的业务开发功能。

 9.3.2 json-server实现数据Mock

json-server是一个node包,可以在不到30秒内获得零编码的完整的Mock服务。

① 项目中安装json-server 

     npm i -D json-server@0.17.3

② 准备一个json文件

③ 添加启动命令

④ 访问接口进行测试

 9.4 整体路由设计

 1、两个以及路由(Layout 、 new)

 2、两个二级路由(Layout— month 、year)

 

 9.5 antD主题色定制

 9.6 Redux管理账目列表

 

 

 9.7 TabBar功能实现

         需求:使用antD的TabBar标签栏进行布局以及路由的切换。

         实现方式:看文档(找到相似Demo—复制代码跑通—定制化修改)

         Layout—index.js

import { Outlet, useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getBillList } from "@/store/modules/billStore";
import "./index.scss";

import {
  BillOutline,
  CalculatorOutline,
  AddCircleOutline,
} from "antd-mobile-icons";
import { TabBar } from "antd-mobile";

const tabs = [
  {
    key: "/",
    title: "月度账单",
    icon: <BillOutline />,
  },
  {
    key: "/new",
    title: "记账",
    icon: <AddCircleOutline />,
  },
  {
    key: "/year",
    title: "年度账单",
    icon: <CalculatorOutline />,
  },
];

const Layout = () => {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(getBillList());
  }, [dispatch]);

  //切换路由菜单
  const navigate = useNavigate();
  const switchRoute = (path) => {
    navigate(path);
  };

  return (
    <div className="layout">
      <div className="container">
        <Outlet />
      </div>

      <div className="footer">
        <TabBar onChange={switchRoute}>
          {tabs.map((item) => (
            <TabBar.Item key={item.key} icon={item.icon} title={item.title} />
          ))}
        </TabBar>
      </div>
    </div>
  );
};
export default Layout;

9.8 统计区域

Month —— index.js

import { NavBar, DatePicker } from "antd-mobile";
import { useState, useMemo, useEffect } from "react";
import classNames from "classnames";
import "./index.scss";
import dayjs from "dayjs";
import { useSelector } from "react-redux";
import _ from "lodash";

const Month = () => {
  //按月做数据分组
  const billList = useSelector((state) => state.bill.billList);
  const monthGroup = useMemo(() => {
    //return出去计算之后的值
    //根据年月进行数据分组
    return _.groupBy(billList, (item) => dayjs(item.date).format("YYYY-MM"));
  }, [billList]);
  //控制弹框的打开和关闭
  const [dateVisible, setDateVisible] = useState(false);

  //控制时间显示
  const [currentDate, setCurrentDate] = useState(() => {
    return dayjs(new Date()).format("YYYY-MM");
  });

  const [currentMonthList, setMonthList] = useState([]);
  const monthResult = useMemo(() => {
    //支出    /收入     /结余
    const pay = currentMonthList
      .filter((item) => item.type === "pay")
      .reduce((a, c) => a + c.money, 0);
    const income = currentMonthList
      .filter((item) => item.type === "income")
      .reduce((a, c) => a + c.money, 0);
    return { pay, income, total: pay + income };
  }, [currentMonthList]);

  //初始化渲染统计数据,把当前月的统计数据显示出来
  useEffect(() => {
    const nowDate = dayjs().format("YYYY-MM")
    setMonthList(monthGroup[nowDate] ?? [])
  }, [monthGroup]);

  //点击确认回调
  const onConfirm = (date) => {
    setDateVisible(false);
    //其他逻辑
    const formatDate = dayjs(date).format("YYYY-MM");
    setMonthList(monthGroup[formatDate] ?? []);
    setCurrentDate(formatDate);
  };
  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date" onClick={() => setDateVisible(true)}>
            <span className="text">{currentDate + ""}月账单</span>
            {/* 思路:根据当前弹框打开的状态控制expand类名是否存在 */}
            <span
              className={classNames("arrow", dateVisible && "expand")}
            ></span>
          </div>
          {/* 统计区域 */}
          <div className="twoLineOverview">
            <div className="item">
              <span className="money">{monthResult.pay.toFixed(2)}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.income.toFixed(2)}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.total.toFixed(2)}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={dateVisible}
            onCancel={() => setDateVisible(false)}
            onConfirm={onConfirm}
            onClose={() => setDateVisible(false)}
            max={new Date()}
          />
        </div>
      </div>
    </div>
  );
};

export default Month;

9.9 列表区域

src— pages— Month — index.js

import { NavBar, DatePicker } from "antd-mobile";
import { useState, useMemo, useEffect } from "react";
import classNames from "classnames";
import "./index.scss";
import dayjs from "dayjs";
import { useSelector } from "react-redux";
import _ from "lodash";
import DayBill from "./components/DayBill";

const Month = () => {
  //按月做数据分组
  const billList = useSelector((state) => state.bill.billList);
  const monthGroup = useMemo(() => {
    //return出去计算之后的值
    //根据年月进行数据分组
    return _.groupBy(billList, (item) => dayjs(item.date).format("YYYY-MM"));
  }, [billList]);

  //控制弹框的打开和关闭
  const [dateVisible, setDateVisible] = useState(false);

  //控制时间显示
  const [currentDate, setCurrentDate] = useState(() => {
    return dayjs(new Date()).format("YYYY-MM");
  });

  const [currentMonthList, setMonthList] = useState([]);
  const monthResult = useMemo(() => {
    //支出    /收入     /结余
    const pay = currentMonthList
      .filter((item) => item.type === "pay")
      .reduce((a, c) => a + c.money, 0);
    const income = currentMonthList
      .filter((item) => item.type === "income")
      .reduce((a, c) => a + c.money, 0);
    return { pay, income, total: pay + income };
  }, [currentMonthList]);

  //初始化渲染统计数据,把当前月的统计数据显示出来
  useEffect(() => {
    const nowDate = dayjs().format("YYYY-MM");
    setMonthList(monthGroup[nowDate] ?? []);
  }, [monthGroup]);

  //点击确认回调
  const onConfirm = (date) => {
    setDateVisible(false);
    //其他逻辑
    const formatDate = dayjs(date).format("YYYY-MM");
    setMonthList(monthGroup[formatDate] ?? []);
    setCurrentDate(formatDate);
  };

  //当前月按照日来做分组
  const dayGroup = useMemo(() => {
    const groupData = _.groupBy(currentMonthList, (item) =>
      dayjs(item.date).format("YYYY-MM-DD")
    );
    const keys = Object.keys(groupData);
    return {
      groupData,
      keys,
    };
  }, [currentMonthList]);

  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date" onClick={() => setDateVisible(true)}>
            <span className="text">{currentDate + ""}月账单</span>
            {/* 思路:根据当前弹框打开的状态控制expand类名是否存在 */}
            <span
              className={classNames("arrow", dateVisible && "expand")}
            ></span>
          </div>
          {/* 统计区域 */}
          <div className="twoLineOverview">
            <div className="item">
              <span className="money">{monthResult.pay.toFixed(2)}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.income.toFixed(2)}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.total.toFixed(2)}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={dateVisible}
            onCancel={() => setDateVisible(false)}
            onConfirm={onConfirm}
            onClose={() => setDateVisible(false)}
            max={new Date()}
          />
        </div>
        {/* 单日列表统计 */}
        {dayGroup.keys.map((key) => {
          return (
            <DayBill key={key} date={key} billList={dayGroup.groupData[key]} />
          );
        })}
      </div>
    </div>
  );
};

export default Month;

src— pages— Month — components— DayBill —— index.js

//月度账单详情
import Icon from "@/components/Icon";
import "./index.scss";
import classNames from "classnames";
import { useMemo } from "react";
import { useState } from "react";
import { billTypeToName } from '@/contants/index'

const DayBill = ({ date, billList }) => {
  const dayResult = useMemo(() => {
    //支出    /收入     /结余
    const pay = billList
      .filter((item) => item.type === "pay")
      .reduce((a, c) => a + c.money, 0);
    const income = billList
      .filter((item) => item.type === "income")
      .reduce((a, c) => a + c.money, 0);
    return { pay, income, total: pay + income };
  }, [billList]);

  //控制展开收起
  const [visible, setVisible] = useState(false);

  return (
    <div className={classNames("dailyBill")}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{date}</span>
          {/* expand 有个类名 站开点箭头朝上*/}
          <span
            className={classNames("arrow", visible && "expand")}
            onClick={() => setVisible(!visible)}
          ></span>
        </div>
        <div className="oneLineOverview">
          <div className="pay">
            <span className="type">支出</span>
            <span className="money">{dayResult.pay.toFixed(2)}</span>
          </div>
          <div className="icon">
            <span className="type">收入</span>
            <span className="money">{dayResult.income.toFixed(2)}</span>
          </div>
          <div className="balance">
            <span className="money">{dayResult.total.toFixed(2)}</span>
            <span className="type">结余</span>
          </div>
        </div>
      </div>
      {/*单日列表*/}
      <div className="billList" style={{ display: visible ? "block" : "none" }}>
        {billList.map((item) => {
          return (
            <div className="bill" key={item.id}>
              {/* 图标 */}
              <Icon type={item.useFor} />
              <div className="detail">
                <div className="billType">{billTypeToName[item.useFor]}</div>
              </div>
              <div className={classNames("money", item.type)}>
                {item.money.toFixed(2)}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default DayBill;

9.10 新增账单

src — store — billStore.js 

src — pages — New — indexjs

import { Button, Input, NavBar, DatePicker } from "antd-mobile";
import classNames from "classnames";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { useState } from "react";
import dayjs from "dayjs";
import { billListData } from "@/contants";
import Icon from "@/components/Icon";
import "./index.scss";
import { addBillList } from "@/store/modules/billStore";


const New = () => {
  const navigate = useNavigate();
  // 1. 准备一个控制收入支出的状态
  const [billType, setBillType] = useState("pay"); // pay-支出 income-收入

  // 收集金额
  const [money, setMoney] = useState(0);
  const moneyChange = (value) => {
    setMoney(value);
  };

  // 收集账单类型
  const [useFor, setUseFor] = useState("");
  const dispatch = useDispatch();
  // 保存账单
  const saveBill = () => {
    // 收集表单数据
    const data = {
      type: billType,
      money: billType === "pay" ? -money : +money,
      date: date,
      useFor: useFor,
    };
    console.log(data);
    dispatch(addBillList(data));
  };
  // 存储选择的时间
  const [date, setDate] = useState();
  // 控制时间打开关闭
  const [dateVisible, setDateVisible] = useState(false);
  // 确认选择时间
  const dateConfirm = (value) => {
    console.log(value);
    setDate(value);
    setDateVisible(false);
  };
  return (
    <div className="keepAccounts">
      <NavBar className="nav" onBack={() => navigate(-1)}>
        记一笔
      </NavBar>

      <div className="header">
        <div className="kaType">
          <Button
            shape="rounded"
            className={classNames(billType === "pay" ? "selected" : "")}
            onClick={() => setBillType("pay")}
          >
            支出
          </Button>
          <Button
            className={classNames(billType === "income" ? "selected" : "")}
            shape="rounded"
            onClick={() => setBillType("income")}
          >
            收入
          </Button>
        </div>

        <div className="kaFormWrapper">
          <div className="kaForm">
            <div className="date">
              <Icon type="calendar" className="icon" />
              <span className="text" onClick={() => setDateVisible(true)}>
                {dayjs(date).format("YYYY-MM-DD")}
              </span>
              {/* 时间选择器 */}
              <DatePicker
                className="kaDate"
                title="记账日期"
                max={new Date()}
                visible={dateVisible}
                onConfirm={dateConfirm}
                onClose={() => setDateVisible(false)}
                onCancel={() => setDateVisible(false)}
              />
            </div>
            <div className="kaInput">
              <Input
                className="input"
                placeholder="0.00"
                type="number"
                value={money}
                onChange={moneyChange}
              />
              <span className="iconYuan">¥</span>
            </div>
          </div>
        </div>
      </div>

      <div className="kaTypeList">
        {/* 数据区域 */}
        {billListData[billType].map((item) => {
          return (
            <div className="kaType" key={item.type}>
              <div className="title">{item.name}</div>
              <div className="list">
                {item.list.map((item) => {
                  return (
                    // selected
                    <div
                      className={classNames(
                        "item",
                        useFor === item.type ? "selected" : ""
                      )}
                      key={item.type}
                      onClick={() => setUseFor(item.type)}
                    >
                      <div className="icon">
                        <Icon type={item.type} />
                      </div>
                      <div className="text">{item.name}</div>
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>

      <div className="btns">
        <Button className="btn save" onClick={saveBill}>
          保 存
        </Button>
      </div>
    </div>
  );
};
export default New;

  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值