项目-头条

头条笔记

1.移动端rem解决多屏适配

介绍

原因:

这个项目是移动端,所以,它需要去适配不同的手机屏幕。我们希望实现适配的效果是:与屏幕大小相关。以按钮为例:在大屏手机,按钮的宽高都大些,在小屏手机上尺寸小些

分析:

Vant 中的样式默认使用px作为单位,如果需要使用rem单位,推荐使用以下两个工具一起工作,来达到目标。-官网地址

  1. 将px单位转换rem单位 利用 postcss 和 postcss-pxtorem 插件将px自动转化成rem
  2. rem是相对于根标签的字体大小改变 利用 amfe-flexible插件 设置根标签字体大小(生产依赖)

步骤

1.安装包

yarn add postcss postcss-pxtorem@5.1.1 -D
# 生产依赖
# 后处理器 开发阶段使用
# 作用:把px单位自动转成rem单位

yarn add amfe-flexible
# 开发依赖
# 修改rem基准值的js插件   需要在打包后需要使用
# 作用: 根据设置屏幕的宽度去调整rem的值(html标签上font-size的大小)
#      它的默认计算方式是屏幕宽度的1/10,默认值是37.5

2.设置postcss

根目录下创建postcss.config.js文件,内容如下:

module.exports = {
  plugins: {
    'postcss-pxtorem': {
      // 能够把所有元素的px单位转成Rem
      // rootValue: 转换px的基准值。
      // 例如一个元素宽是75px,则换成rem之后就是2rem。
      rootValue: 37.5,
      propList: ['*']
    }
  }
}
重启服务器
npm run serve
yarn  serve

3.引入flexible

入口文件main.js导入 amfe-flexible

import 'amfe-flexible'

2.封装axios请求函数 通过awaitTo改进

介绍

原因:

为了调用ajax方便,这里我们先对 axios进行一次封装,把它封装成一个独立的模块,在需要的时候直接加载使用。

思路:

  1. 安装axios包
  2. 对axios进行二次封装

步骤

1.安装包

yarn add axios

2.二次封装axios

  1. src目录下创建 utils/request.js
1.引入axios
import axios from 'axios'

2.设置基地址
//两种方式

//第一种设置基地址方式
const ajax = axios.create({
  //  baseURL: 'http://api-toutiao-web.itheima.net'
  baseURL: 'http://toutiao-app.itheima.net' // 请求的基础路径
})
//第二种设置基地址方式
axios2.defaults.baseURL = 'http://toutiao-app.itheima.net'


3.导出请求
export default (url, method = 'GET', data = {}, params = {}, headers) => ajax({
    url,
    method,
    data,
    params,
    headers
})
创建一个箭头函数 
形参 是需要请求的数据
函数体内调用axios  
相当于ajax({url,method,data,params,headers})
箭头函数  一行代码不用{}return

// 以后如果换网络请求的库, 直接更换这个文件里的代码, 保证代码的复用性和独立性(高内聚低耦合)

3.await错误处理

原因:

await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

所以await只能拿到到Promise.then的结果,不能拿到Promise.catch的结果

拓展复习:Promise里要放异步任务

解决方法awaitTo

Promise 介绍

  1. then( )或catch( )都会在原地返回一个全新的promise对象
  2. then和catch函数体里的return,都会把值返回给原地生成的promise的then函数
export default ({ url, method = 'GET', params, data, headers }) => {
  const pA = axios({ 
    url: url,
    method: method,
    params: params,
    data: data,
    headers: headers
  })
  const pB = pA.then((res) => {
    // 错误对象, 成功对象
    return [null, res]
  }).catch((err) => {
    return [err, null]
  })
  return pB
}

解释:

  1. 发起axios请求在原地留下一个promise对象pA
  2. pA.then且catch( )会在原地留下一个promise对象pB
  3. pA的then且catch的return会把值传给pB的then

3.vant组件

注意:

  1. 在使用vant组件时,不要忘了引入他的css样式

    import 'vant/lib/index.css'

  2. 在使用定制主题2和3时把引入的

    import 'vant/lib/index.less'

vant定制主题方式

方法一

审查元素的方式,查看对应样式的class类名,进行样式覆盖 不生效类名前加 /deep/

​ 图-定制主题方式一

dc9929dabc54e156cf93e042aa63e9d3

方法二:

按照官方文档通过定制主题的方式,直接覆盖vant组件库里less变量

// 这里要把 .css 后缀名改为 .less
import 'vant/lib/index.less'
不然样式失效,

bb0e90a8fae998947bee029817450480

方法三

通过定制主题的方式,自定义less主题文件,基于文件的方式覆盖默认的less变量

在styles文件下新建一个less文件用来覆盖变量

@blue: #007bff;
@white: white;
@font-14: 14px;

// NavBar
@nav-bar-background-color: @blue;
@nav-bar-title-text-color: @white;
@nav-bar-title-font-size: @font-14;

在vue.config.js下添加如下配置

const path = require('path')

// 自定义主题的文件路径
const coverPath = path.join(__dirname, './src/styles/cover.less')

module.exports = {
  css: {
    loaderOptions: {
      less: {
        modifyVars: {
          // 通过 less 文件覆盖(文件路径为绝对路径)
          hack: `true; @import "${coverPath}";`
        }
      }
    }
  }
}

4.ESlint关闭规则

//不让变量名内有_下划线

步骤

  1. ESlint官网查询
  2. 在ESlint.js文件下修改

5.Token

1.存储Token

vuex文件夹下

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    Token: null//定义数据
  },
  mutations: {
      //修改数据的方法
      //参数1 :state
      //参数2 :接收传过来的数据
    tonkenFN (state, Token) {
      state.Token = Token
    }
  },
  actions: {
  },
  modules: {
  }
})

接口调用处

<script>
import { mapMutations } from 'vuex'
//map映射Mutations下的方法
// from 来自  vuex
export default {
  data () {
    return {
      token: 'asdfadsfasdfwer'
    }
  },
  methods: {
      //展开注意必须写[]  和  ''包裹
    ...mapMutations(['tonkenFN']),
    buFn () {
      this.tonkenFN(this.token)
    }
  }
}
</script>

2.持久化存储Token

原因:

网页刷新,所有变量回归初始化 ,vuex的值就消失 用户就得重新登录

解决:

把token数据保存在本地

步骤

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
      //2.token的取值先看本地存储  没有  为空
    Token: localStorage.getItem('token') || null
  },
  mutations: {
    tonkenFN (state, Token) {
      state.Token = Token
        //1.把token数据传入vuex中也将其存入localStorage
        //注意: localStorage.setItem后跟的是()不是=
        //参数1 :键名
        //参数2 :值
      localStorage.setItem('token', Token)
       
    }
  },
  actions: {
  },
  modules: {
  }
})

6.路由守卫

需求 :

  1. 已经登陆了,不要再跳转到登录页(如果想重新登录,点击退出)
  2. 涉及用户操作的页面,需要登录(编辑,点赞,评论)

7.切换路由页面

代码实现

<van-tabbar v-model="active" router>
  <van-tabbar-item icon="home-o" to="/home">首页</van-tabbar-item>
  <van-tabbar-item icon="setting-o" to="/user">我的</van-tabbar-item>
</van-tabbar>

步骤 :

  1. van-tabbar 添加入router属性

image-20210709160927314

V-自带的属性

  1. van-tabbar-item 添加入to="/home"

原生实现

<router-link  to="/home">首页</router-link>
//router-link 相当于一个a标签

8.首页-频道列表

展示图

image-20210709190907681

接口图

344758aee624befff5dc3b4b087814e2

代码

/api/index.js文件下
import store from '@/store'
//引入vuex  以前都是map映射 那是vue页面
//js页面  直接引入  store文件夹下的index文件
export const UserListAPI = () => {
  return request({
    url: '/v1_0/search',
    headers:{
Authorization = 'Bearer ' + store.state.token
        //注意:'Bearer ' 加空格 加空格 加空格
    }
  })
}

9.首页 - 文章列表

需求1 封装组件

展示图

image-20210709194114375

将文章列表封装成一个单独的组件

步骤

  1. 在Home/components/ArticleList.vue //文章列表组件–只负责循环展示数据
  2. 首页—引入文章列表组件,替换Tab中间作为插槽使用

需求2.先铺静态页面(不考虑三张图片的问题)

展示图

image-20210709200224570

步骤

  1. 简单的文章列表铺设( 不考虑图片)
  2. 封装网络请求

image-20210709202459869

当前系统时间: new Date( ).getTime( ) 毫秒值

page 传几 返回第几页数据

  1. (封装的展示组件/vant组件-都是只负责展示你传入的数据, 而交互的动作交回页面来完成-规范)

  2. 引入请求->创建数组->存储数据

  3. 父传子->将数组数据传给->文章列表组件

需求3.实现图片(1~3)图

  1. 先实现一张图片的展示( 观察数据)

image-20210710134456981

  1. 由后端数据可知:cover里的type是图片的数量,images[ ]存储图片地址
  2. 渲染图片数据(实现一张图片展示)—v-if判断cover是否等于1 等于1展示

image-20210710134702593

  1. 考虑三张图片的位置,渲染页面(多张图片v-for渲染)

image-20210710135158107

10.首页-下拉刷新

步骤:

  1. 找到组件van-pull-refresh, main.js引入注册

  2. 包住ArticleList组件(内容列表)

  3. 获取新的数据, getArticleList方法-发请求拿新数据

  4. isLoading改成false(关闭顶部加载中状态-保证下次还能继续下拉刷新)

image-20210710142807360

11首页-上拉加载

image-20210710162413739

BUG 1 下拉刷新

上拉加载更多.page++ 加载到page =3(或n )

这个时候,用户进行 下拉刷新 由于当前page=3

渲染的数据会是 第3页的

解决

在下拉时—把page改为 1

BUG 2 网页打开发起两次请求

问题:

  • 第一次 : 在created里调用发起的 请求第一页
  • 第二次 : list组件挂载到页面(但是第一页的数据没有回来,list没有高度,list认为触底了,所以马上执行onload)
  • 请求了第2页

解决:

list组件添加一个v-if判断, 如果渲染list的数组的长度大于0就让list组件渲染

12.首页-频道点击切换

频道切换

思路:

思路来源于用户操作

要实现点击频道切换页面?

点击频道—删除原数组,请求新数组, 渲染页面

根据什么请求不同的数据?

获取频道列表的接口 传id 的 传的id不同返回的数据不同

如何获取点击 频道的id呢?

看看vant组件文档

步骤

  1. 点击频道,拿到id

在tabs组件

image-20210710172927926

click 事件返回 点击的频道的 name 和 title

点击

image-20210710174854038

所以 name 返回的是索引 title 返回的是标题

但是 我想要的是 id ( 请求频道标题的数据中有id) 怎么办

给 name属性设置动态属性 :name=“obj.id”

把name属性默认的属性换成id

现在点击频道 返回的就是click的回调就是id 和标题

  1. data 中创建channelId用于存储id 默认值 0

    1. 在发起获取文章列表的数据中 当做参数传入(即可通过id获取不同的数据)

    2. click的方法里 将获取的 id 赋给 channelId

    3. 把page设置为1 切换频道页码回到1,数据重新获取

    4. 将渲染文章数组 清空

    5. 重新调用 获取文章列表的方法

13.格式化时间

dayjs 中文官网:https://dayjs.fenxianglu.cn/

  1. 下载包
yarn add dayjs
  1. 使用页面 引入dayjsrelativeTime本地化语言包
// 导入 dayjs 的核心模块
import dayjs from 'dayjs'

// 导入计算相对时间的插件
import relativeTime from 'dayjs/plugin/relativeTime'

// 导入本地化的语言包
import  'dayjs/locale/zh'
  1. 配置插件和语言包
// 配置插件
dayjs.extend(relativeTime)
// 配置语言包
dayjs.locale('zh')
  1. 定义一个格式化时间的方法

    /所有步骤
    import dayjs from 'dayjs'
    import relativeTime from 'dayjs/plugin/relativeTime'
    import zh from 'dayjs/locale/zh-cn'
    
    export default {
        
      formatTime(时间参数){
          dayjs.extend( relativeTime)
          dayjs.locale('zh')
          var a=dayjs()
          var b=dayjs(时间参数 )
          returan a.to(b)
      }  
    }
    
  2. 预处理数组

image-20210710194519023

res.data.data.results.forEach( obj=>obj.pubdate=this.formatTime(obj.pubdate)
)

//循环数组  将处理完的数据在赋值给原数组

14.图片懒加载

图片懒加载:当图片滚动到视口区域,在加载

基于vant 组件实现

import { Lazyload } from 'vant'

Vue.use(Lazyload)


// 注册时可以配置额外的选项
Vue.use(Lazyload, {
  lazyComponent: true
})//这句话是对模块进行懒加载

<img v-for="img in imageList" v-lazy="img" />
//用v-lazy 代替src
//img 是 图片地址
export default {
  data() {
    return {
      imageList: [
        'https://img01.yzcdn.cn/vant/apple-1.jpg',
        'https://img01.yzcdn.cn/vant/apple-2.jpg',
      ],
    };
  },
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFEtP9Li-1625989206635)(C:\Users\醒醒\AppData\Roaming\Typora\typora-user-images\image-20210710202216673.png)]

15.反馈面板

一级面板

使用: 反馈组件 ActionSheet面板

注意: 使用 get-container 指定挂载的节点

image-20210711073236737

否则就会出现这种情况

解决

//ActionSheet组件标签上
get-container="body"

二级面板

二级面板

思路:

点击 反馈垃圾内容时 给控制ActionSheet组件 反馈面板的数组 替换

image-20210711074417485

使用三个数组

数组1 : 用来存放 当前反馈级别的内容(默认是 一级反馈的内容)

数组2 : 也用来存放 一级反馈的内容

数组3 : 用来存放 二级反馈的内容

思路:

默认是一级反馈的内容

点击 反馈垃圾内容时 替换数组 将数组3赋值给数组1

点击 取消 将数组 2 赋值给数组 1

代码实现

image-20210711075400282

通过名字判断你点击的是哪一个选项,

后续步骤

  1. 看接口文档,api下定义反馈的方法
  2. 文章列表组件里写反馈组件标签
  3. 网络请求要写在index.vue 里
  4. 子传父 用 文章列表组件 将 反馈对象和文章id 传出
  5. 调用后台接口
this.$emit( 'action',action,this.artId)
//子传父 三个参数

16.数字过大问题

原因:

js 中的 安全数字 是一个 16位的数字

image-20210711081741587

大于这个数会出错image-20210711081853017

解决方案:

json-bigint(https://www.npmjs.com/package/json-bigint)

步骤

  1. 下载json-bigint包

    yarn add json-bigint
    
  2. 在request…js中使用, 对后台的数据, 不要让axios自动转成JS对象, 需要用json-bigint转换

    import bigInt from 'json-bigint'
    
    const ajax = axios.create({
      baseURL: 'http://toutiao-app.itheima.net', // 请求的基础路径
      transformResponse: [function (data) { // 对内容进行处理
        // data:就是本次请求获取的数据
        // 在这里可以对它进行进一步的处理 -- JSONbig
        // 后端返回数据可能不是 JSON 字符串,而JSONbig.parse()只能处理JSON字符串
        // 所以,为了保证代码可以正常执行,这里引入try-catch来捕获异常
        try {
          // 尝试着进行大数的处理
          return bigInt.parse(data)
        } catch {
          // 大数处理失败时的后备方案
          return JSON.parse(data)
        }
      }]
    })
    

解释

为什么要写在request.js中?

image-20210711113930839

axios再把数据(JSON字符串)请求回来以后,帮你把JSON字符串 自动转换 JS数据类型

如果 数字大于16位 则16位后面的精度会出错

所以 我们要在 request.js 中修改


怎么实现?

对axios进行重构,自己来编写转换JSON字符串的过程

/transformResponse 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
    		//data 就是本次请求回来的数据
			 // 对 data 进行任意转换处理
		 return data;
		 }]

参考下面axios官网截图

axios官网:(http://www.axios-js.com/zh-cn/docs/#axios-create-config)

  1. **axios.create([config])**干什么的

image-20210711120142941

const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
    //设置基地址
  timeout: 1000,
     //`timeout 指定请求超时的毫秒数(0 表示无超时时间)
  // 如果请求话费了超过timeout的时间,请求将被中断
  headers: {'X-Custom-Header': 'foobar'}
     // headers 是即将被发送的自定义请求头
});
  1. transformRequest干什么的

     // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
      transformResponse: [function (data) {
          //data 就是本次请求回来的数据
        // 对 data 进行任意转换处理
        return data;
      }],
    
          也就是在本次请求回来后 ,但是还没有传递给js之前
    对请求回来的数据进行处理
    
  2. try…catch

image-20210711132711953

  1. 首先,执行try { …} 中的代码
  2. 如果这里没有错误,则忽略catch( error):执行到try的末尾并跳过catch继续执行
  3. 如果这里出现了错误,则try停止执行,控制流转向catch(err)的开头,(变量err可以使用任何名称),将包含一个error对象,该对象包含了所发生的事件的详细信息

完整代码 第二行有疑问

import axios2 from 'axios'
const bigInt = require('json-bigint')({ storeAsString: true }) 
const axios = axios2.create({ 
  transformResponse: [function (data) { 
    if (data.length === 0) {
      // 如果data响应数据没有, JSON.parse('')会报错-JSON格式错误
      return ''
    } else {
      try {
        // 尝试着进行大数的处理
        // 把JSON字符串, 转成JS数据类型 (相比JSON.parse来说, 它会对大数进行处理)
        // 把大数转成BigInt类型对象, 而并不是普通的Number
        // 后台给我的: 1324194433473708032
        // JSON.parse转换会变成 1324194433473708000
        // bigInt会转成 BigInt对象(正常显示这个大数)
        return bigInt.parse(data)
      } catch {
        // 大数处理失败时的后备方案
        return JSON.parse(data)
      }
    }
  }]
})

文章列表删除思路

思路1 :

  1. 删除成功
  2. 把对应数据删除(删除一条数据 页面剩余9条数据)
  3. 问题 可能会删除光了,造成 白屏 体验不好
  4. **解决 ** 和后台商量 ,删除一个文章,接口一定要返回一个新的(补在后面)

思路2 :

  1. 删除后重新调用接口,重新调接口-拿回10个新的数据

共同点 :

  1. 使page=1
  2. 清空渲染数组
  3. 重新请求列表数据

17.axios拦截器

请求拦截器

过程: 前端(客户端) => axios =>axios拦截器触发 => 后端服务器

使用场景 可以在发起请求之前,带一些值

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });


响应拦截器

过程: 响应拦截器:前端(客户端) <=axios <=axios响应器触发 <= 后端服务器

使用场景 处理响应状态(401 /…)统一处理

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    //当状态码2xx/3xx开头时进入这里
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 响应状态码 4xx/5xx进这里
    // 对响应错误做点什么
    return Promise.reject(error);
  });

响应拦截器 使用场景 拦截401错误请求

  1. 手动修改token值,模拟401错误
  2. console.dir(err) 观看数据
    1. image-20210711142401360
    2. image-20210711142504126

详细代码


axios.interceptors.response.use(function (response) { // 
  return response
}, function (error) { 
  if (error.response.status === 401) { // 身份过期
    // token无用,
    store.commit('setToken', '')
    router.push({ path: '/login' })
  }
  return Promise.reject(error)
})
/处理401情况, 遇到401自动切换到登录页面

登录页面

​ 登录页

image-20210709153813491

步骤

V- :代表vant里面的组件

  1. 静态页面
  2. 表单及页面-表单校验(V-form )
  3. 点击提交发起请求 (表单的name是提交的参数)
  4. 登录成功和失败提示 (V-notify )
  5. 给登录按钮设置 禁用加载动画 (V-button)
  6. 把token值存到vuex中
  7. token持久化保存
  8. 登录成功路由跳转到首页

首页

image-20210709153956102

image-20210709154046823

步骤一 底部

  1. 底部—导入tabbar组件(V-tabbar)
  2. 底部—创建首页的二级页面Home和User/index.vue
  3. 底部—router/index.js -引入二级页面组件 -配置二级路由
  4. 底部—首页设置挂载点
  5. 底部—tabbar上设置router和to属性,点击首页我的切换路由

步骤二 顶部

  1. 顶部—index.vue使用NavBar(V-NavBar)
  2. 顶部—NavBar插槽技术-自定义左侧图片
  3. 顶部—右侧icon图标导入,修改颜色(V-Icon)

步骤三 中间内容

  1. 中间—导航栏切换 -Tabs标签页组件-注册复制(V-Tabs)
  2. 中间—定义接口方法,请求频道数据
  3. 中间—在当前页面使用接口拿到数据铺设的页面
  4. 中间—封装 展示文章单元格列表组件
  5. 引入注册组件, 替换到Tab中间作为插槽使用

文章单元格列表组件 内

  1. 简单的文章列表铺设(不考虑图片)

  2. title和label区域-标签+css

  3. 回到index.vue - 请求数据—(封装的展示组件/vant组件-都是只负责展示你传入的数据, 而交互的动作交回页面来完成-规范)

  4. 封装文章列表数据API接口并引入

  5. 定义方法,先请求默认的推荐数据

  6. 创建数组,接收请求回来的数据

  7. 传入数组到子组件文章列表组件—>铺设列表单元格 (父传子)

技巧及小知识点

1.快速创建多个文件夹

mkdir 后面接文件夹名 ,同时多个 空格间隔

mkdir api styles utils

2.绝对路径

1.不要手动写绝对路径

const path = require('path')

// 自定义主题的文件路径
const coverPath = path.join(__dirname, './src/styles/cover.less'

3.路由跳转

this.$router.push({ path:'/layout'})

4.传参数

传参数的时候最好使用对象

对象是无序的只要名字符合就可以

5.尽快熟悉项目结构

画一个树形图显示

写出每个文件夹都是干什么的

6.规范

封装的组件/vant组件-都是只负责展示你传入的 数据,而交互的动作交回给页面完成

7.获取当前系统时间

  1. new Date( )
  2. new Date( ).getTime( ) 毫秒值

8.postcss处理css

注意 : postcss只会处理css代码,标签里的行内样式单位需要你自己写rem

9.图片403解决

有的网站会对图片做防盗链

防盗链:后台判断你请求的来源,是不是自己服务器发起的请求,如果不是,不返回数据

解决

public文件夹/index.html/html标签下的head
<meat name="referrer" content="no-referrer" >

10.在组件内绑定事件注意

在组件内绑定的任何事件都是自定义事件,(除了那些组件自己原有的事件)

11.事件修饰符 .native

@click.native="btn"
就是实现原生的click

12.数据预处理

数据与处理:在请求回来的数据使用之前,先把数据里要处理的数据拿出来完善

13.代码优化小技巧

  1. 函数内多层if-else 使用return 优化

    代码如下:

     actionFn (actionObj, artId) {
    
          if (actionObj.name === '拉黑作者') {
            return // 只要你敢进来, 我就让下面代码不执行
          }
    
          if (actionObj.name === '不感兴趣') {
            const [err] = await articleDislikeAPI({
              target: artId
            })
            return
          }
        },
    
  2. 一模一样的代码重复使用封装函数

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值